Repository: CnCNet/xna-cncnet-client
Branch: develop
Commit: 05d6760f6287
Files: 482
Total size: 3.4 MB
Directory structure:
gitextract_dgog0ci0/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.yml
│ │ ├── config.yml
│ │ └── feature-request.yml
│ ├── copilot-coding-agent-setup.md
│ ├── copilot-instructions.md
│ └── workflows/
│ ├── build.yml
│ ├── copilot-setup-steps.yml
│ ├── pr-build-comment.yml
│ └── release-build.yml
├── .gitignore
├── .gitmodules
├── AdditionalFiles/
│ ├── UpdateServerScripts/
│ │ ├── preupdateexec
│ │ └── updateexec
│ └── VersionFileWriter/
│ └── VersionConfig.ini
├── ClientCore/
│ ├── CCIniFile.cs
│ ├── ClientConfiguration.cs
│ ├── ClientCore.csproj
│ ├── Enums/
│ │ ├── AllowPrivateMessagesFromEnum.cs
│ │ ├── ClientType.cs
│ │ ├── ClientTypeHelper.cs
│ │ └── SortDirection.cs
│ ├── Extensions/
│ │ ├── ArrayExtensions.cs
│ │ ├── EnumExtensions.cs
│ │ ├── EnumerableExtensions.cs
│ │ ├── FileExtensions.cs
│ │ ├── IniFileExtensions.cs
│ │ └── StringExtensions.cs
│ ├── I18N/
│ │ ├── Translation.cs
│ │ └── TranslationGameFile.cs
│ ├── INIProcessing/
│ │ ├── IniPreprocessInfoStore.cs
│ │ ├── IniPreprocessor.cs
│ │ └── PreprocessorBackgroundTask.cs
│ ├── LoadingScreenController.cs
│ ├── OSVersion.cs
│ ├── PlatformShim/
│ │ └── EncodingExt.cs
│ ├── ProcessLauncher.cs
│ ├── ProfanityFilter.cs
│ ├── ProgramConstants.cs
│ ├── SavedGameManager.cs
│ ├── Settings/
│ │ ├── BoolSetting.cs
│ │ ├── DoubleSetting.cs
│ │ ├── IIniSetting.cs
│ │ ├── INISetting.cs
│ │ ├── IntRangeSetting.cs
│ │ ├── IntSetting.cs
│ │ ├── StringListSetting.cs
│ │ ├── StringSetting.cs
│ │ └── UserINISettings.cs
│ └── Statistics/
│ ├── DataWriter.cs
│ ├── GameParsers/
│ │ └── LogFileStatisticsParser.cs
│ ├── GenericMatchParser.cs
│ ├── GenericStatisticsManager.cs
│ ├── MatchStatistics.cs
│ ├── PlayerStatistics.cs
│ └── StatisticsManager.cs
├── ClientGUI/
│ ├── ClientGUI.csproj
│ ├── ClientGUICreator.cs
│ ├── DarkeningPanel.cs
│ ├── GameProcessLogic.cs
│ ├── HotkeyConfigurationWindow.cs
│ ├── ICompositeControl.cs
│ ├── IME/
│ │ ├── DummyIMEHandler.cs
│ │ ├── IMEHandler.cs
│ │ ├── SdlIMEHandler.cs
│ │ └── WinFormsIMEHandler.cs
│ ├── INIConfigException.cs
│ ├── INItializableWindow.cs
│ ├── IToolTipContainer.cs
│ ├── Parser.cs
│ ├── ScreenResolution.cs
│ ├── Settings/
│ │ ├── FileSettingCheckBox.cs
│ │ ├── FileSettingDropDown.cs
│ │ ├── FileSourceDestinationInfo.cs
│ │ ├── IFileSetting.cs
│ │ ├── IUserSetting.cs
│ │ ├── SettingCheckBox.cs
│ │ ├── SettingCheckBoxBase.cs
│ │ ├── SettingDropDown.cs
│ │ └── SettingDropDownBase.cs
│ ├── ToolTip.cs
│ ├── TranslationGUIExtensions.cs
│ ├── TranslationINIParser.cs
│ ├── UIDesignConstants.cs
│ ├── XNAChatTextBox.cs
│ ├── XNAClientButton.cs
│ ├── XNAClientCheckBox.cs
│ ├── XNAClientColorDropDown.cs
│ ├── XNAClientDropDown.cs
│ ├── XNAClientLinkLabel.cs
│ ├── XNAClientPreferredItemDropDown.cs
│ ├── XNAClientStateButton.cs
│ ├── XNAClientTabControl.cs
│ ├── XNAClientToggleButton.cs
│ ├── XNAExtraPanel.cs
│ ├── XNALinkButton.cs
│ ├── XNAMessageBox.cs
│ ├── XNAOptionsPanel.cs
│ ├── XNAPlayerSlotIndicator.cs
│ ├── XNAWindow.cs
│ └── XNAWindowBase.cs
├── ClientUpdater/
│ ├── ClientUpdater.csproj
│ ├── Compression/
│ │ ├── Common/
│ │ │ ├── CRC.cs
│ │ │ ├── CommandLineParser.cs
│ │ │ ├── InBuffer.cs
│ │ │ └── OutBuffer.cs
│ │ ├── CompressionHelper.cs
│ │ ├── ICoder.cs
│ │ ├── LZ/
│ │ │ ├── IMatchFinder.cs
│ │ │ ├── LzBinTree.cs
│ │ │ ├── LzInWindow.cs
│ │ │ └── LzOutWindow.cs
│ │ ├── LZMA/
│ │ │ ├── LzmaBase.cs
│ │ │ ├── LzmaDecoder.cs
│ │ │ └── LzmaEncoder.cs
│ │ └── RangeCoder/
│ │ ├── RangeCoder.cs
│ │ ├── RangeCoderBit.cs
│ │ └── RangeCoderBitTree.cs
│ ├── CustomComponent.cs
│ ├── UpdateMirror.cs
│ ├── Updater.cs
│ ├── UpdaterFileInfo.cs
│ └── VersionState.cs
├── CommonAssemblies.txt
├── CommonAssembliesNetFx.txt
├── Contributing.md
├── DXClient.slnx
├── DXMainClient/
│ ├── AdminRestarter.cs
│ ├── DXGUI/
│ │ ├── Campaign/
│ │ │ ├── CampaignCheckBox.cs
│ │ │ ├── CampaignDropDown.cs
│ │ │ ├── CampaignSelector.cs
│ │ │ ├── CampaignTagSelector.cs
│ │ │ └── CheaterWindow.cs
│ │ ├── GameClass.cs
│ │ ├── Generic/
│ │ │ ├── DropDownDataWriteMode.cs
│ │ │ ├── ExtrasWindow.cs
│ │ │ ├── GameInProgressWindow.cs
│ │ │ ├── GameLoadingWindow.cs
│ │ │ ├── GameSessionCheckBox.cs
│ │ │ ├── GameSessionDropDown.cs
│ │ │ ├── LoadingScreen.cs
│ │ │ ├── MainMenu.cs
│ │ │ ├── ManualUpdateQueryWindow.cs
│ │ │ ├── OptionPanels/
│ │ │ │ ├── AudioOptionsPanel.cs
│ │ │ │ ├── CnCNetOptionsPanel.cs
│ │ │ │ ├── ComponentsPanel.cs
│ │ │ │ ├── DisplayOptionsPanel.cs
│ │ │ │ ├── GameOptionsPanel.cs
│ │ │ │ └── UpdaterOptionsPanel.cs
│ │ │ ├── OptionsWindow.cs
│ │ │ ├── PrivacyNotification.cs
│ │ │ ├── StatisticsWindow.cs
│ │ │ ├── TopBar.cs
│ │ │ ├── URLHandler.cs
│ │ │ ├── UpdateQueryWindow.cs
│ │ │ └── UpdateWindow.cs
│ │ ├── IGameSessionSetting.cs
│ │ ├── IMessageView.cs
│ │ ├── ISwitchable.cs
│ │ └── Multiplayer/
│ │ ├── ChatListBox.cs
│ │ ├── CnCNet/
│ │ │ ├── ChoiceNotificationBox.cs
│ │ │ ├── CnCNetGameLoadingLobby.cs
│ │ │ ├── CnCNetLobby.cs
│ │ │ ├── CnCNetLoginWindow.cs
│ │ │ ├── GameCreationEventArgs.cs
│ │ │ ├── GameCreationWindow.cs
│ │ │ ├── GlobalContextMenu.cs
│ │ │ ├── GlobalContextMenuData.cs
│ │ │ ├── LoadOrSaveGameOptionPresetWindow.cs
│ │ │ ├── MapSharingConfirmationPanel.cs
│ │ │ ├── PasswordRequestWindow.cs
│ │ │ ├── PrivateMessageNotificationBox.cs
│ │ │ ├── PrivateMessagingPanel.cs
│ │ │ ├── PrivateMessagingWindow.cs
│ │ │ ├── RecentPlayerTable.cs
│ │ │ ├── RecentPlayerTableRightClickEventArgs.cs
│ │ │ ├── TunnelListBox.cs
│ │ │ └── TunnelSelectionWindow.cs
│ │ ├── GameFiltersPanel.cs
│ │ ├── GameInformationIconOnlyPanel.cs
│ │ ├── GameInformationIconPanel.cs
│ │ ├── GameInformationPanel.cs
│ │ ├── GameListBox.cs
│ │ ├── GameLoadingLobbyBase.cs
│ │ ├── GameLobby/
│ │ │ ├── ChatBoxCommand.cs
│ │ │ ├── CnCNetGameLobby.cs
│ │ │ ├── CommandHandlers/
│ │ │ │ ├── CommandHandlerBase.cs
│ │ │ │ ├── IntCommandHandler.cs
│ │ │ │ ├── IntNotificationHandler.cs
│ │ │ │ ├── NoParamCommandHandler.cs
│ │ │ │ ├── NotificationHandler.cs
│ │ │ │ └── StringCommandHandler.cs
│ │ │ ├── CoopBriefingBox.cs
│ │ │ ├── GameHostInactiveChecker.cs
│ │ │ ├── GameLaunchButton.cs
│ │ │ ├── GameLeftEventArgs.cs
│ │ │ ├── GameLobbyBase.cs
│ │ │ ├── GameLobbyCheckBox.cs
│ │ │ ├── GameLobbyDropDown.cs
│ │ │ ├── GameLobbySettingsEventArgs.cs
│ │ │ ├── GameLobbySettingsWindow.cs
│ │ │ ├── GameModeMapFilter.cs
│ │ │ ├── GameType.cs
│ │ │ ├── LANGameLobby.cs
│ │ │ ├── MapCodeHelper.cs
│ │ │ ├── MapPreviewBox.cs
│ │ │ ├── MultiplayerGameLobby.cs
│ │ │ ├── PlayerLocationIndicator.cs
│ │ │ └── SkirmishLobby.cs
│ │ ├── LANGameCreationWindow.cs
│ │ ├── LANGameLoadingLobby.cs
│ │ ├── LANLobby.cs
│ │ ├── LANLobbyBroadcastManager.cs
│ │ ├── LANLobbyBroadcastMessageReceivedEventArgs.cs
│ │ ├── LANMessageDeduplicator.cs
│ │ ├── LANPlayerManager.cs
│ │ ├── PlayerExtraOptionsPanel.cs
│ │ ├── PlayerListBox.cs
│ │ ├── TeamStartMappingPanel.cs
│ │ └── TeamStartMappingsPanel.cs
│ ├── DXMainClient.csproj
│ ├── Domain/
│ │ ├── CustomMissionHelper.cs
│ │ ├── DirectDrawCompatibilityChecker.cs
│ │ ├── DirectDrawWrapper.cs
│ │ ├── DirectDrawWrapperManager.cs
│ │ ├── DiscordHandler.cs
│ │ ├── FinalSunSettings.cs
│ │ ├── MainClientConstants.cs
│ │ ├── Mission.cs
│ │ ├── Multiplayer/
│ │ │ ├── AllianceHolder.cs
│ │ │ ├── CacheManagerBase.cs
│ │ │ ├── CnCNet/
│ │ │ │ ├── CnCNetGame.cs
│ │ │ │ ├── CnCNetPlayerCountTask.cs
│ │ │ │ ├── CnCNetTunnel.cs
│ │ │ │ ├── CustomCnCNetGame.cs
│ │ │ │ ├── DefaultCnCNetGame.cs
│ │ │ │ ├── GameCollection.cs
│ │ │ │ ├── HostedCnCNetGame.cs
│ │ │ │ ├── MapEventArgs.cs
│ │ │ │ ├── MapSharer.cs
│ │ │ │ ├── NameValidator.cs
│ │ │ │ ├── SHA1EventArgs.cs
│ │ │ │ ├── TimedHttpClient.cs
│ │ │ │ └── TunnelHandler.cs
│ │ │ ├── CoopHouseInfo.cs
│ │ │ ├── CoopMapInfo.cs
│ │ │ ├── CustomMapCache.cs
│ │ │ ├── GameMode.cs
│ │ │ ├── GameModeMap.cs
│ │ │ ├── GameModeMapBase.cs
│ │ │ ├── GameModeMapCollection.cs
│ │ │ ├── GameOptionPresets.cs
│ │ │ ├── GenericHostedGame.cs
│ │ │ ├── ICacheManager.cs
│ │ │ ├── IGameModeMap.cs
│ │ │ ├── IMapPreviewCacheManager.cs
│ │ │ ├── IReadOnlyGameModeMapCollection.cs
│ │ │ ├── LAN/
│ │ │ │ ├── ClientIntCommandHandler.cs
│ │ │ │ ├── ClientNoParamCommandHandler.cs
│ │ │ │ ├── ClientStringCommandHandler.cs
│ │ │ │ ├── HostedLANGame.cs
│ │ │ │ ├── LANClientCommandHandler.cs
│ │ │ │ ├── LANColor.cs
│ │ │ │ ├── LANLobbyUser.cs
│ │ │ │ ├── LANPlayerInfo.cs
│ │ │ │ ├── LANServerCommandHandler.cs
│ │ │ │ ├── NetworkMessageEventArgs.cs
│ │ │ │ ├── ServerNoParamCommandHandler.cs
│ │ │ │ └── ServerStringCommandHandler.cs
│ │ │ ├── Map.cs
│ │ │ ├── MapChangeEventArgs.cs
│ │ │ ├── MapFileEventArgs.cs
│ │ │ ├── MapFileWatcher.cs
│ │ │ ├── MapLoader.cs
│ │ │ ├── MapPreviewCacheManager.cs
│ │ │ ├── MapPreviewExtractor.cs
│ │ │ ├── MultiplayerColor.cs
│ │ │ ├── PlayerExtraOptions.cs
│ │ │ ├── PlayerHouseInfo.cs
│ │ │ ├── PlayerInfo.cs
│ │ │ ├── SavedGamePlayer.cs
│ │ │ ├── TeamStartMapping.cs
│ │ │ └── TeamStartMappingPreset.cs
│ │ └── SavedGame.cs
│ ├── Online/
│ │ ├── Channel.cs
│ │ ├── ChannelUser.cs
│ │ ├── ChatMessage.cs
│ │ ├── CnCNetGameCheck.cs
│ │ ├── CnCNetManager.cs
│ │ ├── CnCNetUserData.cs
│ │ ├── Connection.cs
│ │ ├── EventArguments/
│ │ │ ├── AttemptedServerEventArgs.cs
│ │ │ ├── CTCPEventArgs.cs
│ │ │ ├── ChannelCTCPEventArgs.cs
│ │ │ ├── ChannelEventArgs.cs
│ │ │ ├── ChannelModeEventArgs.cs
│ │ │ ├── ChannelTopicEventArgs.cs
│ │ │ ├── CnCNetPrivateMessageEventArgs.cs
│ │ │ ├── ConnectionLostEventArgs.cs
│ │ │ ├── FavoriteMapEventArgs.cs
│ │ │ ├── GameOptionPresetEventArgs.cs
│ │ │ ├── JoinUserEventArgs.cs
│ │ │ ├── KickEventArgs.cs
│ │ │ ├── MultiplayerNameRightClickedEventArgs.cs
│ │ │ ├── PrivateCTCPEventArgs.cs
│ │ │ ├── PrivateMessageEventArgs.cs
│ │ │ ├── ServerMessageEventArgs.cs
│ │ │ ├── UnreadMessageCountEventArgs.cs
│ │ │ ├── UserAwayEventArgs.cs
│ │ │ ├── UserListEventArgs.cs
│ │ │ └── WhoEventArgs.cs
│ │ ├── FileHashCalculator.cs
│ │ ├── IConnectionManager.cs
│ │ ├── IRCColor.cs
│ │ ├── IRCUser.cs
│ │ ├── IUserCollection.cs
│ │ ├── PrivateMessageHandler.cs
│ │ ├── PrivateMessageUser.cs
│ │ ├── QueuedMessage.cs
│ │ ├── QueuedMessageType.cs
│ │ ├── RecentPlayer.cs
│ │ ├── Server.cs
│ │ ├── SortedUserCollection.cs
│ │ └── UnsortedUserCollection.cs
│ ├── PreStartup.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── Resources/
│ │ ├── ClientDefinitions.ini
│ │ ├── DTA/
│ │ │ ├── CampaignSelector.ini
│ │ │ ├── CheaterScreen.ini
│ │ │ ├── CnCNetGameLobby.ini
│ │ │ ├── CnCNetLobby.ini
│ │ │ ├── Compatibility/
│ │ │ │ ├── Configs/
│ │ │ │ │ ├── aqrit.cfg
│ │ │ │ │ ├── cnc-ddraw.ini
│ │ │ │ │ ├── ddraw-auto.ini
│ │ │ │ │ ├── ddraw-gdi.ini
│ │ │ │ │ ├── ddraw-opengl.ini
│ │ │ │ │ └── dxwnd.ini
│ │ │ │ └── Unix/
│ │ │ │ ├── wine-game.bat
│ │ │ │ ├── wine-game.sh
│ │ │ │ ├── wine-mapedit.bat
│ │ │ │ └── wine-mapedit.sh
│ │ │ ├── Default Theme/
│ │ │ │ ├── DTACnCNetClient.ini
│ │ │ │ ├── MainMenuTheme.bak
│ │ │ │ ├── MainMenuTheme.ogg
│ │ │ │ ├── MainMenuTheme.wma
│ │ │ │ └── MainMenuTheme.xnb
│ │ │ ├── ExtrasWindow.ini
│ │ │ ├── FScompatfix.sdb
│ │ │ ├── GameCollectionConfig.ini
│ │ │ ├── GameLobbyBase.ini
│ │ │ ├── GameOptions.ini
│ │ │ ├── GenericWindow.ini
│ │ │ ├── KeyboardCommands.ini
│ │ │ ├── LANGameLobby.ini
│ │ │ ├── LANLobby.ini
│ │ │ ├── LoadingScreen.ini
│ │ │ ├── MainMenu.ini
│ │ │ ├── MultiplayerGameLobby.ini
│ │ │ ├── OptionsWindow.ini
│ │ │ ├── ReShade Files/
│ │ │ │ └── ReShade.ini
│ │ │ ├── Renderers.ini
│ │ │ ├── SkirmishLobby.ini
│ │ │ ├── SpriteFont0.xnb
│ │ │ ├── SpriteFont1.xnb
│ │ │ ├── SpriteFont2.xnb
│ │ │ ├── SpriteFont3.xnb
│ │ │ ├── SpriteFont4.xnb
│ │ │ ├── StatisticsWindow.ini
│ │ │ ├── UpdaterConfig.ini
│ │ │ ├── UserDefaults.ini
│ │ │ ├── ZLIB.License.txt
│ │ │ ├── ZLIB.Ms-PL.txt
│ │ │ ├── arrow.cur
│ │ │ ├── cnc-ddraw.ini
│ │ │ ├── compatfix.sdb
│ │ │ ├── cursor.cur
│ │ │ ├── ddrawcompat.ini
│ │ │ ├── l480s01.pcx
│ │ │ ├── l480s02.pcx
│ │ │ ├── l480s11.pcx
│ │ │ ├── l480s12.pcx
│ │ │ ├── l600s01.pcx
│ │ │ ├── l600s02.pcx
│ │ │ ├── l600s11.pcx
│ │ │ ├── l600s12.pcx
│ │ │ ├── qres license.txt
│ │ │ ├── ts-ddraw-gdi.ini
│ │ │ └── ts-ddraw.ini
│ │ ├── INI/
│ │ │ ├── Base/
│ │ │ │ └── Instructions.txt
│ │ │ ├── Battle.ini
│ │ │ ├── Default.ini
│ │ │ ├── FSR.ini
│ │ │ ├── Game Options/
│ │ │ │ ├── Auto Deploy MCV.ini
│ │ │ │ ├── Disable Super Weapons.ini
│ │ │ │ ├── Disable Unit Queueing.ini
│ │ │ │ ├── Disable Visceroids.ini
│ │ │ │ ├── Extreme AI.ini
│ │ │ │ ├── Harder AI.ini
│ │ │ │ ├── Immune Harvesters.ini
│ │ │ │ ├── Infinite Tiberium.ini
│ │ │ │ ├── Ingame Allying.ini
│ │ │ │ ├── Instant Harvester Unload.ini
│ │ │ │ ├── Naval.ini
│ │ │ │ ├── No Baddy Crates.ini
│ │ │ │ ├── No Crew.ini
│ │ │ │ ├── No Silos.ini
│ │ │ │ ├── Replace Tiberium With Ore.ini
│ │ │ │ ├── Reveal Shroud.ini
│ │ │ │ ├── Shroud Regrows.ini
│ │ │ │ ├── Starting Units.ini
│ │ │ │ ├── Storms.ini
│ │ │ │ ├── Turbo Vehicles.ini
│ │ │ │ ├── Turtling AI.ini
│ │ │ │ ├── Uncrushable Infantry.ini
│ │ │ │ └── Veteran Balance Patch.ini
│ │ │ ├── MPMaps.ini
│ │ │ ├── Map Code/
│ │ │ │ ├── Difficulty Easy.ini
│ │ │ │ ├── Difficulty Hard.ini
│ │ │ │ ├── Difficulty Medium.ini
│ │ │ │ ├── King of the Hill.ini
│ │ │ │ ├── Naval Only AI.ini
│ │ │ │ ├── Scavenger.ini
│ │ │ │ └── Survivor.ini
│ │ │ ├── MapSel.ini
│ │ │ ├── MapSel01.ini
│ │ │ ├── Menu.ini
│ │ │ ├── ai.ini
│ │ │ ├── aifs.ini
│ │ │ ├── art.ini
│ │ │ ├── artfs.ini
│ │ │ ├── day.ini
│ │ │ ├── dusk.ini
│ │ │ ├── firestrm.ini
│ │ │ ├── ion.ini
│ │ │ ├── keyboard.ini
│ │ │ ├── morning.ini
│ │ │ ├── night.ini
│ │ │ ├── rules.ini
│ │ │ ├── snow.ini
│ │ │ ├── sound.ini
│ │ │ ├── sound01.ini
│ │ │ ├── temperat.ini
│ │ │ ├── theme.ini
│ │ │ └── tutorial.ini
│ │ ├── Map Editor/
│ │ │ └── test.txt
│ │ ├── Maps/
│ │ │ └── Custom/
│ │ │ └── custom maps.txt
│ │ └── SUN.ini
│ ├── Startup.cs
│ ├── app.PerMonitorV2.manifest
│ └── app.SystemAware.manifest
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── Docs/
│ ├── Build.md
│ ├── DiscordRichPresence.md
│ ├── HowToUpdate.md
│ ├── INISystem.md
│ ├── Migration-INI.md
│ ├── Migration.md
│ ├── NewFeatures.md
│ ├── Translation.md
│ └── Updater.md
├── GitVersion.yml
├── LICENSE
├── NuGet.config
├── README.md
├── References/
│ ├── .gitkeep
│ └── Facepunch.Steamworks.2.4.1.nupkg
├── Scripts/
│ ├── Build.bat
│ ├── ClearBinAndObjDirs.bat
│ ├── Get-CommonAssemblyList.ps1
│ ├── README.md
│ └── build.ps1
├── SecondStageUpdater/
│ ├── Program.cs
│ └── SecondStageUpdater.csproj
├── TranslationNotifierGenerator/
│ ├── StringExtensions.cs
│ ├── TranslationNotifierGenerator.cs
│ └── TranslationNotifierGenerator.csproj
└── global.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
# All files
[*]
indent_style = space
# Xml files
[*.xml]
indent_size = 2
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
[*.{cs,vb}]
# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:warning
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# CA1806: Do not ignore method results
dotnet_diagnostic.CA1806.severity = error
#### C# Coding Conventions ####
[*.cs]
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
[*.{cs,vb}]
# Naming rules
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
# Symbol specifications
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
*.ps1 eol=lf
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: cncnet
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: Bug Report
description: Open an issue to ask for a XNA Client bug to be fixed.
title: "Your bug report title here"
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
> [!WARNING]
> Before posting an issue, please read the **checklist at the bottom**.
Thanks for taking the time to fill out this bug report! If you need real-time help, join us on the [C&C Mod Haven Discord](https://discord.gg/Smv4JC8BUG) server in the __#xna-client-chat__ channel.
Please make sure you follow these instructions and fill in every question with as much detail as possible.
- type: textarea
id: description
attributes:
label: Description
description: |
Write a detailed description telling us what the issue is, and if/when the bug occurs.
validations:
required: true
- type: input
id: xna-client-version
attributes:
label: XNA Client Version
description: |
What version of XNA Client are you using? Please provide a link to the exact XNA Client build used, especially if it's not a release build.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps To Reproduce
description: |
Tell us how to reproduce this issue so the developer(s) can reproduce the bug.
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behaviour
description: |
Tell us what should happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behaviour
description: |
Tell us what actually happens instead.
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional Context
description: |
Attach additional files or links to content related to the bug report here, like:
- images/gifs/videos to illustrate the bug;
- files or ini configs that are needed to reproduce the bug;
- a client log (mandatory if you're submitting a crash report).
- type: checkboxes
id: checks
attributes:
label: Checklist
description: Please read and ensure you followed all the following options.
options:
- label: The issue happens on the **latest official** version of XNA Client and wasn't fixed yet.
required: true
- label: I agree to elaborate the details if requested and provide thorough testing if the bugfix is implemented.
required: true
- label: I added a very descriptive title to this issue.
required: true
- label: I used the GitHub search and read the issue list to find a similar issue and didn't find it.
required: true
- label: I have attached as much information as possible *(screenshots, gifs, videos, client logs, etc)*.
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Official channels on C&C Mod Haven
url: https://discord.gg/Smv4JC8BUG
about: If you want to discuss something with us without filing an issue.
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.yml
================================================
name: Feature Request
description: Open an issue to ask for a XNA Client feature to be implemented.
title: "Your feature request title here"
labels: ["feature"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! If you need real-time help, join us on the [C&C Mod Haven Discord](https://discord.gg/Smv4JC8BUG) server in the __#xna-client-chat__ channel.
- type: textarea
id: description
attributes:
label: Description
description: |
Write a detailed description telling us what the feature you want to be implemented in the XNA Client.
validations:
required: true
================================================
FILE: .github/copilot-coding-agent-setup.md
================================================
# GitHub Copilot coding agent setup instructions
This section only applies to the GitHub Copilot coding agent, running in a Linux runner from the GitHub Action environment. It does not apply to other environments, such as local development.
The GitHub Actions workflow `.github/workflows/copilot-setup-steps.yml` runs the setup steps mentioned in this file automatically. The commands below are the manual equivalent and **should only be run if you encounter a build failure** — for example, if GitVersion cannot determine the version, if submodules are missing, or if NuGet restore fails.
## Step 1 — Initialize git submodules
`Rampastring.XNAUI` (and its nested submodule `Rampastring.Tools`) may not be pre-initialized. Missing them causes compile errors about unknown `Rampastring.*` types.
```shell
git submodule update --init --recursive
```
## Step 2 — Unshallow the clone and fetch `develop`
The build system uses **GitVersion.MsBuild** to compute version numbers at compile time. It requires two things:
- A full (non-shallow) commit history.
- The `develop` branch reachable as a remote-tracking ref (it is the mainline branch in `GitVersion.yml`). Without it, any branch that is not `develop` or `master` fails with `Gitversion could not determine which branch to treat as the development branch`.
Run all three commands unconditionally:
- `--unshallow` is a no-op on an already-full clone (`|| true` prevents it from aborting).
- `set-branches` resets the remote's fetch refspec to the standard glob `+refs/heads/*:refs/remotes/origin/*`, removing any single-branch refspec that a shallow clone may have injected. Without this, LibGit2Sharp (used by GitVersion 5.12.0) crashes with `ref 'refs/remotes/origin/develop' doesn't match the destination` because it iterates refspecs in order and fails on the first non-matching one instead of falling through to the glob.
- The final fetch brings `refs/remotes/origin/develop` into the local ref store through that glob refspec so GitVersion can find it.
The same fix must be applied to every submodule recursively: `Rampastring.XNAUI` and its nested `Rampastring.Tools` submodule also carry `GitVersion.MsBuild` and are subject to the same crash when checked out with a narrow single-branch refspec.
```shell
git fetch --unshallow origin || true
git remote set-branches origin '*'
git fetch origin develop
git submodule foreach --recursive \
'git fetch --unshallow origin || true; git remote set-branches origin "*"; git fetch origin'
```
## Step 3 — Restore NuGet packages
Run restore from the **repo root** so that the solution file (`DXClient.slnx`) is used. This ensures all projects — including `SecondStageUpdater`, which the build pulls in transitively — are restored. Always pass the `Configuration` property; omitting it picks the wrong target frameworks.
```shell
dotnet restore -p:Configuration=UniversalGLRelease
```
## Step 4 — Build
```shell
dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0 --no-restore
```
A successful build ends with `0 Error(s)`.
================================================
FILE: .github/copilot-instructions.md
================================================
# Agent Instructions
## General information
### Project structure
| Path | Description |
|------|-------------|
| `DXMainClient/` | Main entry-point project — always the build target |
| `ClientCore/` | Core game-client logic |
| `ClientGUI/` | UI layer |
| `ClientUpdater/` | Auto-updater logic |
| `SecondStageUpdater/` | Secondary updater executable |
| `Rampastring.XNAUI/` | UI framework (git submodule) |
| `GitVersion.yml` | GitVersion branch and versioning strategy |
| `global.json` | Pins the required .NET SDK version (10.0, any feature band) |
| `Directory.Build.props` | MSBuild properties shared across all projects |
| `Directory.Packages.props` | Central NuGet package version management |
| `Docs/Build.md` | Human-oriented build documentation |
### Build the project
```shell
dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0
```
A successful build ends with `0 Error(s)`.
### Contributing guidelines
See [Contributing.md](../Contributing.md) for coding style, formatting, and other contribution guidelines. Be aware, Copilot, you MUST read and follow this file, even if the user did not explicitly ask you to.
## GitHub Copilot coding agent setup instructions
This section only applies to the GitHub Copilot coding agent, running in a Linux runner from the GitHub Action environment. It does not apply to other environments, such as local development.
The steps in the [copilot-coding-agent-setup.md](./copilot-coding-agent-setup.md) file are automatically executed via a GitHub Action workflow before the agent starts. **Only read and run them manually if you encounter a build failure**.
================================================
FILE: .github/workflows/build.yml
================================================
name: build client
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]
workflow_dispatch:
jobs:
build-clients:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
global-json-file: ./global.json
- name: Build
run: ./Scripts/build.ps1
shell: pwsh
- uses: actions/upload-artifact@v4
name: Upload Artifacts
with:
name: artifacts
path: ./Compiled
================================================
FILE: .github/workflows/copilot-setup-steps.yml
================================================
name: Copilot setup steps
# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code with full history and submodules
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
# the set-branches call is required to collapse the shallow-clone's specific-branch refspec back to the glob, preventing a LibGit2Sharp crash in GitVersion 5.12.0
- name: Unshallow clone and fetch develop branch
run: |
git fetch --unshallow origin || true
git remote set-branches origin '*'
git fetch origin develop
# Apply the same fix to every submodule (including nested ones), because GitVersion.MsBuild
# runs against each submodule directory that carries it, and the same LibGit2Sharp refspec
# crash occurs there when the submodule was checked out with a narrow single-branch refspec.
git submodule foreach --recursive \
'git fetch --unshallow origin || true; git remote set-branches origin "*"; git fetch origin'
- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: ./global.json
- name: Restore NuGet packages
run: dotnet restore -p:Configuration=UniversalGLRelease
- name: Build
run: dotnet build DXMainClient/DXMainClient.csproj -p:Configuration=UniversalGLRelease -f net8.0 --no-restore
================================================
FILE: .github/workflows/pr-build-comment.yml
================================================
name: automatic comment on pull request
on:
workflow_run:
workflows: ['build client']
types: [completed]
jobs:
pr_comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-22.04
steps:
- uses: actions/github-script@v6
with:
# This snippet is public-domain, taken from
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
script: |
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};
const pull_head_sha = '${{github.event.workflow_run.head_sha}}';
const pull_user_id = ${{github.event.sender.id}};
const issue_number = await (async () => {
const pulls = await github.rest.pulls.list({owner, repo});
for await (const {data} of github.paginate.iterator(pulls)) {
for (const pull of data) {
if (pull.head.sha === pull_head_sha && pull.user.id === pull_user_id) {
return pull.number;
}
}
}
})();
if (issue_number) {
core.info(`Using pull request ${issue_number}`);
} else {
return core.error(`No matching pull request found`);
}
const {data: {artifacts}} = await github.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id});
if (!artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Nightly build for this pull request:\n`;
for (const art of artifacts) {
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
body += `\nThis comment is automatic and is meant to allow guests to get latest automatic builds without registering. It is updated on every successful build.`;
const {data: comments} = await github.rest.issues.listComments({repo, owner, issue_number});
const existing_comment = comments.find((c) => c.user.login === 'github-actions[bot]');
if (existing_comment) {
core.info(`Updating comment ${existing_comment.id}`);
await github.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body});
} else {
core.info(`Creating a comment`);
await github.rest.issues.createComment({repo, owner, issue_number, body});
}
================================================
FILE: .github/workflows/release-build.yml
================================================
name: release build
on:
release:
types: [published]
permissions:
contents: write
jobs:
build:
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
global-json-file: ./global.json
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0
with:
versionSpec: '5.x'
- name: Determine Version
uses: gittools/actions/gitversion/execute@v0
- name: Development Build Check
if: "!github.event.release.prerelease"
shell: pwsh
run: |
if ($env:GitVersion_CommitsSinceVersionSource -ne "0") {
Write-Output "::error:: This is a development build and should not be released. Did you forget to create a new tag for the release?"
exit 1
}
- name: Build
run: ./Scripts/build.ps1
shell: pwsh
- name: Zip Artifact
run: 7z a -t7z -mx=9 -m0=lzma2 -ms=on -r -- ${{ format('xna-cncnet-client-{0}.7z', env.GitVersion_SemVer) }} ./Compiled/*
shell: pwsh
- name: Upload Final Artifact to the Release
uses: softprops/action-gh-release@v2
with:
append_body: true
files: ${{ format('xna-cncnet-client-{0}.7z', env.GitVersion_SemVer) }}
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# ... with an exception in References folder
!**/[Rr]eferences/*.nupkg
!**/[Rr]eferences/*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# CnCNet
Compiled/
Compiled*/
# Käyttiksen (Mac ja win) tekemiä tiedostoja joita jättää pois
._*
.DS_Store*
.Spotlight-V100
.Trashes
ehthumbs.db
Icon?
[Tt]humbs.db
# Dolphin
.directory
.idea/
# Game specific build prop files
Directory.Build.Game.*.props
================================================
FILE: .gitmodules
================================================
[submodule "Rampastring.XNAUI"]
path = Rampastring.XNAUI
url = https://github.com/CnCNet/Rampastring.XNAUI.git
================================================
FILE: AdditionalFiles/UpdateServerScripts/preupdateexec
================================================
[Rename]
OLD_FILE_PATH=NEW_FILE_PATH
[Delete]
FILE_PATH
[RenameFolder]
OLD_FOLDER_PATH=NEW_FOLDER_PATH
[RenameAndMerge]
OLD_FOLDER_PATH=NEW_FOLDER_PATH
[ForceDeleteFolder]
FOLDER_PATH
[DeleteFolderIfEmpty]
FOLDER_PATH
================================================
FILE: AdditionalFiles/UpdateServerScripts/updateexec
================================================
[Rename]
OLD_FILE_PATH=NEW_FILE_PATH
[Delete]
FILE_PATH
[RenameFolder]
OLD_FOLDER_PATH=NEW_FOLDER_PATH
[RenameAndMerge]
OLD_FOLDER_PATH=NEW_FOLDER_PATH
[ForceDeleteFolder]
FOLDER_PATH
[DeleteFolderIfEmpty]
FOLDER_PATH
================================================
FILE: AdditionalFiles/VersionFileWriter/VersionConfig.ini
================================================
; Mod version.
[Version]
1
; Mod updater version.
; Will prompt (either in update status or actual dialog prompt, see below for ManualDownloadURL) for a manual update download if set on server version file and mismatched between local & server.
; Omit or set to N/A if not wishing to use this feature.
[UpdaterVersion]
N/A
; If set client will show a dialog prompting for manual download with the provided link if a manual update download is required.
; Omit if wishing to not use this feature.
[ManualDownloadURL]
[Options]
; If set, enables the extended updater features such as archives, updater version and manual download URL.
EnableExtendedUpdaterFeatures=yes
; If set, will go through every subdirectory recursively for directories given in Include.
RecursiveDirectorySearch=yes
; If set, will always create two version files - one with everything included (version_base) and the proper, actual version file with only changed files (version).
; version_base should be kept around as it is used to compare which files have been changed next time VersionWriter is ran.
IncludeOnlyChangedFiles=no
; If set, original versions of archived files will also be copied to copied files directory.
CopyArchivedOriginalFiles=no
; If set, any directories (including all files and subdirectories in them, regardless of any other settings) and files flagged as hidden or system protected will be excluded. This also defaults to true.
ExcludeHiddenAndSystemFiles=yes
; If set, the mod version string is treated as .NET timestamp/datetime format string with current local time applied on it.
ApplyTimestampOnVersion=no
; If set, no files will be copied whatsoever, only version file(s) are generated. Setting this also disables archived files feature regardless of other settings.
NoCopyMode=no
; Files & directories to include in version file.
[Include]
test.file
test2.file
Test\
; Files (not directories) to be excluded from included files list.
; User-generated (settings etc), temporary and log files should be listed here.
[ExcludeFiles]
Test\test2.file
; Directories to be excluded from included files list
; If you include entire directory trees f.ex map editors, this is useful to exclude things like autosave or log directories.
[ExcludeDirectories]
Test\TestDir
; Files (not directories) to be included as archives.
[ArchiveFiles]
test.file
; Custom components. ID's and filenames are normally hardcoded, but also overridable through UpdaterConfig.ini.
[AddOns]
COMPONENT_ID=customcomp.mix
================================================
FILE: ClientCore/CCIniFile.cs
================================================
using Rampastring.Tools;
using System.IO;
namespace ClientCore
{
public class CCIniFile : IniFile
{
public CCIniFile(string path) : base(path)
{
foreach (IniSection section in Sections)
{
string baseSectionName = section.GetStringValue("$BaseSection", null);
if (string.IsNullOrWhiteSpace(baseSectionName))
continue;
var baseSection = Sections.Find(s => s.SectionName == baseSectionName);
if (baseSection == null)
{
Logger.Log($"Base section not found in INI file {path}, section {section.SectionName}, base section name: {baseSectionName}");
continue;
}
int addedKeyCount = 0;
foreach (var kvp in baseSection.Keys)
{
if (!section.KeyExists(kvp.Key))
{
section.Keys.Insert(addedKeyCount, kvp);
addedKeyCount++;
}
}
}
}
protected override void ApplyBaseIni()
{
string basedOnSetting = GetStringValue("INISystem", "BasedOn", string.Empty);
if (string.IsNullOrEmpty(basedOnSetting))
return;
string[] basedOns = basedOnSetting.Split(',');
foreach (string basedOn in basedOns)
ApplyBasedOnIni(basedOn);
}
private void ApplyBasedOnIni(string basedOn)
{
if (string.IsNullOrEmpty(basedOn))
return;
FileInfo baseIniFile;
if (basedOn.Contains("$THEME_DIR$"))
baseIniFile = SafePath.GetFile(basedOn.Replace("$THEME_DIR$", ProgramConstants.GetResourcePath()));
else
baseIniFile = SafePath.GetFile(SafePath.GetFileDirectoryName(FileName), basedOn);
// Consolidate with the INI file that this INI file is based on
if (!baseIniFile.Exists)
Logger.Log(FileName + ": Base INI file not found! " + baseIniFile.FullName);
CCIniFile baseIni = new CCIniFile(baseIniFile.FullName);
ConsolidateIniFiles(baseIni, this);
Sections = baseIni.Sections;
}
}
}
================================================
FILE: ClientCore/ClientConfiguration.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using ClientCore.Enums;
using ClientCore.Extensions;
using ClientCore.I18N;
using Rampastring.Tools;
namespace ClientCore
{
public class ClientConfiguration
{
private const string GENERAL = "General";
private const string AUDIO = "Audio";
private const string SETTINGS = "Settings";
private const string LINKS = "Links";
private const string TRANSLATIONS = "Translations";
private const string USER_DEFAULTS = "UserDefaults";
public const string CLIENT_SETTINGS = "DTACnCNetClient.ini";
public const string GAME_OPTIONS = "GameOptions.ini";
public const string CLIENT_DEFS = "ClientDefinitions.ini";
public const string NETWORK_DEFS_LOCAL = "NetworkDefinitions.local.ini";
public const string NETWORK_DEFS = "NetworkDefinitions.ini";
private static ClientConfiguration _instance;
private IniFile gameOptions_ini;
private IniFile DTACnCNetClient_ini;
private IniFile clientDefinitionsIni;
private IniFile networkDefinitionsIni;
protected ClientConfiguration()
{
var baseResourceDirectory = SafePath.GetDirectory(ProgramConstants.GetBaseResourcePath());
if (!baseResourceDirectory.Exists)
throw new FileNotFoundException($"Couldn't find {CLIENT_DEFS} at {baseResourceDirectory} (directory doesn't exist). Please verify that you're running the client from the correct directory.");
FileInfo clientDefinitionsFile = SafePath.GetFile(baseResourceDirectory.FullName, CLIENT_DEFS);
if (!(clientDefinitionsFile?.Exists ?? false))
throw new FileNotFoundException($"Couldn't find {CLIENT_DEFS} at {baseResourceDirectory}. Please verify that you're running the client from the correct directory.");
clientDefinitionsIni = new IniFile(clientDefinitionsFile.FullName);
DTACnCNetClient_ini = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), CLIENT_SETTINGS));
gameOptions_ini = new IniFile(SafePath.CombineFilePath(baseResourceDirectory.FullName, GAME_OPTIONS));
string networkDefsPathLocal = SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), NETWORK_DEFS_LOCAL);
if (File.Exists(networkDefsPathLocal))
{
networkDefinitionsIni = new IniFile(networkDefsPathLocal);
Logger.Log("Loaded network definitions from NetworkDefinitions.local.ini (user override)");
}
else
{
string networkDefsPath = SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), NETWORK_DEFS);
networkDefinitionsIni = new IniFile(networkDefsPath);
}
RefreshTranslationGameFiles();
}
///
/// Singleton Pattern. Returns the object of this class.
///
/// The object of the ClientConfiguration class.
public static ClientConfiguration Instance
{
get
{
if (_instance == null)
{
_instance = new ClientConfiguration();
}
return _instance;
}
}
public void RefreshSettings()
{
DTACnCNetClient_ini = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), CLIENT_SETTINGS));
}
#region Client settings
private string _mainMenuMusicName = null;
public string MainMenuMusicName => _mainMenuMusicName ??= GetMainMenuMusicName();
private string GetMainMenuMusicName()
{
string raw = DTACnCNetClient_ini.GetStringValue(GENERAL, "MainMenuTheme", "mainmenu");
string[] parts = raw.SplitWithCleanup();
string chosen = parts.Length > 0
? parts[new Random().Next(parts.Length)]
: "mainmenu";
return SafePath.CombineFilePath(chosen);
}
public float DefaultAlphaRate => DTACnCNetClient_ini.GetSingleValue(GENERAL, "AlphaRate", 0.005f);
public float CheckBoxAlphaRate => DTACnCNetClient_ini.GetSingleValue(GENERAL, "CheckBoxAlphaRate", 0.05f);
public float IndicatorAlphaRate => DTACnCNetClient_ini.GetSingleValue(GENERAL, "IndicatorAlphaRate", 0.05f);
#region Color settings
public string UILabelColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "UILabelColor", "0,0,0");
public string UIHintTextColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "HintTextColor", "128,128,128");
public string DisabledButtonColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "DisabledButtonColor", "108,108,108");
public string AltUIColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "AltUIColor", "255,255,255");
public string ButtonHoverColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "ButtonHoverColor", "255,192,192");
public string MapPreviewNameBackgroundColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "MapPreviewNameBackgroundColor", "0,0,0,144");
public string MapPreviewNameBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "MapPreviewNameBorderColor", "128,128,128,128");
public string MapPreviewStartingLocationHoverRemapColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "StartingLocationHoverColor", "255,255,255,128");
public bool MapPreviewStartingLocationUsePlayerRemapColor => DTACnCNetClient_ini.GetBooleanValue(GENERAL, "StartingLocationsUsePlayerRemapColor", false);
public string AltUIBackgroundColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "AltUIBackgroundColor", "196,196,196");
public string WindowBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "WindowBorderColor", "128,128,128");
public string PanelBorderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "PanelBorderColor", "255,255,255");
public string ListBoxHeaderColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "ListBoxHeaderColor", "255,255,255");
public string DefaultChatColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "DefaultChatColor", "0,255,0");
public string AdminNameColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "AdminNameColor", "255,0,0");
public string ReceivedPMColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "PrivateMessageOtherUserColor", "196,196,196");
public string SentPMColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "PrivateMessageColor", "128,128,128");
public int DefaultPersonalChatColorIndex => DTACnCNetClient_ini.GetIntValue(GENERAL, "DefaultPersonalChatColorIndex", 0);
public string ListBoxFocusColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "ListBoxFocusColor", "64,64,168");
public string HoverOnGameColor => DTACnCNetClient_ini.GetStringValue(GENERAL, "HoverOnGameColor", "32,32,84");
public IniSection GetParserConstants() => DTACnCNetClient_ini.GetSection("ParserConstants");
#endregion
#region Tool tip settings
public int ToolTipFontIndex => DTACnCNetClient_ini.GetIntValue(GENERAL, "ToolTipFontIndex", 0);
public int ToolTipOffsetX => DTACnCNetClient_ini.GetIntValue(GENERAL, "ToolTipOffsetX", 0);
public int ToolTipOffsetY => DTACnCNetClient_ini.GetIntValue(GENERAL, "ToolTipOffsetY", 0);
public int ToolTipMargin => DTACnCNetClient_ini.GetIntValue(GENERAL, "ToolTipMargin", 4);
public float ToolTipDelay => DTACnCNetClient_ini.GetSingleValue(GENERAL, "ToolTipDelay", 0.67f);
public float ToolTipAlphaRatePerSecond => DTACnCNetClient_ini.GetSingleValue(GENERAL, "ToolTipAlphaRate", 4.0f);
#endregion
#region Audio options
public float SoundGameLobbyJoinCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, "SoundGameLobbyJoinCooldown", 0.25f);
public float SoundGameLobbyLeaveCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, "SoundGameLobbyLeaveCooldown", 0.25f);
public float SoundMessageCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, "SoundMessageCooldown", 0.25f);
public float SoundPrivateMessageCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, "SoundPrivateMessageCooldown", 0.25f);
public float SoundGameLobbyGetReadyCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, "SoundGameLobbyGetReadyCooldown", 5.0f);
public float SoundGameLobbyReturnCooldown => DTACnCNetClient_ini.GetSingleValue(AUDIO, "SoundGameLobbyReturnCooldown", 1.0f);
#endregion
#endregion
#region Game options
public string Sides => gameOptions_ini.GetStringValue(GENERAL, nameof(Sides), "GDI,Nod,Allies,Soviet");
public string InternalSideIndices => gameOptions_ini.GetStringValue(GENERAL, nameof(InternalSideIndices), string.Empty);
public string SpectatorInternalSideIndex => gameOptions_ini.GetStringValue(GENERAL, nameof(SpectatorInternalSideIndex), string.Empty);
#endregion
#region Client definitions
private string _ClientGameTypeString => clientDefinitionsIni.GetStringValue(SETTINGS, "ClientGameType", string.Empty);
private ClientType? _ClientGameType = null;
public ClientType ClientGameType => _ClientGameType ??= ClientTypeHelper.FromString(_ClientGameTypeString);
public string DiscordAppId => clientDefinitionsIni.GetStringValue(SETTINGS, "DiscordAppId", string.Empty);
public int SendSleep => clientDefinitionsIni.GetIntValue(SETTINGS, "SendSleep", 2500);
public int LoadingScreenCount => clientDefinitionsIni.GetIntValue(SETTINGS, "LoadingScreenCount", 2);
public int ThemeCount => clientDefinitionsIni.GetSectionKeys("Themes").Count;
public string LocalGame => clientDefinitionsIni.GetStringValue(SETTINGS, "LocalGame", "DTA");
public bool SidebarHack => clientDefinitionsIni.GetBooleanValue(SETTINGS, "SidebarHack", false);
public int MinimumRenderWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumRenderWidth", 1280);
public int MinimumRenderHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumRenderHeight", 768);
public int MaximumRenderWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MaximumRenderWidth", 1280);
public int MaximumRenderHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MaximumRenderHeight", 800);
public string[] RecommendedResolutions => clientDefinitionsIni.GetStringListValue(SETTINGS, "RecommendedResolutions",
$"{MinimumRenderWidth}x{MinimumRenderHeight},{MaximumRenderWidth}x{MaximumRenderHeight}");
public string WindowTitle => clientDefinitionsIni.GetStringValue(SETTINGS, "WindowTitle", string.Empty)
.L10N("INI:ClientDefinitions:WindowTitle");
public string InstallationPathRegKey => clientDefinitionsIni.GetStringValue(SETTINGS, "RegistryInstallPath", "TiberianSun");
public string CnCNetLiveStatusIdentifier => clientDefinitionsIni.GetStringValue(SETTINGS, "CnCNetLiveStatusIdentifier", "cncnet5_ts");
public string BattleFSFileName => clientDefinitionsIni.GetStringValue(SETTINGS, "BattleFSFileName", "BattleFS.ini");
public string MapEditorExePath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, "MapEditorExePath", SafePath.CombineFilePath("FinalSun", "FinalSun.exe")));
public string UnixMapEditorExePath => clientDefinitionsIni.GetStringValue(SETTINGS, "UnixMapEditorExePath", Instance.MapEditorExePath);
public bool ModMode => clientDefinitionsIni.GetBooleanValue(SETTINGS, "ModMode", false);
public string LongGameName => clientDefinitionsIni.GetStringValue(SETTINGS, "LongGameName", "Tiberian Sun");
public string LongSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, "LongSupportURL", "https://www.moddb.com/members/rampastring");
public string ShortSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ShortSupportURL", "www.moddb.com/members/rampastring");
public string ChangelogURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ChangelogURL", "https://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/change-log");
public string CreditsURL => clientDefinitionsIni.GetStringValue(SETTINGS, "CreditsURL", "https://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/credits#Rampastring");
public string ManualDownloadURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ManualDownloadURL", string.Empty);
public string FinalSunIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, "FSIniPath", SafePath.CombineFilePath("FinalSun", "FinalSun.ini")));
public int MaxNameLength => clientDefinitionsIni.GetIntValue(SETTINGS, "MaxNameLength", 16);
public bool UseIsometricCells => clientDefinitionsIni.GetBooleanValue(SETTINGS, "UseIsometricCells", true);
public int WaypointCoefficient => clientDefinitionsIni.GetIntValue(SETTINGS, "WaypointCoefficient", 128);
public int MapCellSizeX => clientDefinitionsIni.GetIntValue(SETTINGS, "MapCellSizeX", 48);
public int MapCellSizeY => clientDefinitionsIni.GetIntValue(SETTINGS, "MapCellSizeY", 24);
public bool UseBuiltStatistic => clientDefinitionsIni.GetBooleanValue(SETTINGS, "UseBuiltStatistic", false);
public string WindowedModeKey => clientDefinitionsIni.GetStringValue(SETTINGS, "WindowedModeKey", "Video.Windowed");
public bool CopyResolutionDependentLanguageDLL => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CopyResolutionDependentLanguageDLL", true);
public string StatisticsLogFileName => clientDefinitionsIni.GetStringValue(SETTINGS, "StatisticsLogFileName", "DTA.LOG");
public string[] TrustedDomains => clientDefinitionsIni.GetStringListValue(SETTINGS, "TrustedDomains", string.Empty);
public string[] AlwaysTrustedDomains = { "cncnet.org", "gamesurge.net", "dronebl.org", "discord.com", "discord.gg", "youtube.com", "youtu.be" };
public bool ShowGameIconInGameList => clientDefinitionsIni.GetBooleanValue(SETTINGS, "ShowGameIconInGameList", true);
public (string Name, string Path) GetThemeInfoFromIndex(int themeIndex) => clientDefinitionsIni.GetStringValue("Themes", themeIndex.ToString(), ",").Split(',').AsTuple2();
///
/// Returns the directory path for a theme, or null if the specified
/// theme name doesn't exist.
///
/// The name of the theme.
/// The path to the theme's directory.
public string GetThemePath(string themeName)
{
var themeSection = clientDefinitionsIni.GetSection("Themes");
foreach (var key in themeSection.Keys)
{
var (name, path) = key.Value.Split(',');
if (name == themeName)
return path;
}
return null;
}
public string SettingsIniName => clientDefinitionsIni.GetStringValue(SETTINGS, "SettingsFile", "Settings.ini");
public string TranslationIniName => clientDefinitionsIni.GetStringValue(TRANSLATIONS, nameof(TranslationIniName), "Translation.ini");
public string TranslationsFolderPath => SafePath.CombineDirectoryPath(
clientDefinitionsIni.GetStringValue(TRANSLATIONS, "TranslationsFolder",
SafePath.CombineDirectoryPath("Resources", "Translations")));
private List _translationGameFiles;
public List TranslationGameFiles => _translationGameFiles;
///
/// Force a refresh of the translation game files list.
///
public void RefreshTranslationGameFiles()
{
_translationGameFiles = ParseTranslationGameFiles();
}
///
/// Looks up the list of files to try and copy into the game folder with a translation.
///
/// Source/destination relative path pairs.
/// Thrown when the syntax of the list is invalid.
private List ParseTranslationGameFiles()
{
List gameFiles = new();
if (!clientDefinitionsIni.SectionExists(TRANSLATIONS))
return gameFiles;
foreach (string key in clientDefinitionsIni.GetSectionKeys(TRANSLATIONS))
{
// the syntax is GameFileX=path/to/source.file,path/to/destination.file[,checked]
// where X can be any text or number
if (!key.StartsWith("GameFile"))
continue;
string value = clientDefinitionsIni.GetStringValue(TRANSLATIONS, key, string.Empty);
string[] parts = clientDefinitionsIni.GetStringListValue(TRANSLATIONS, key, string.Empty);
// fail explicitly if the syntax is wrong
if (parts.Length is < 2 or > 3
|| (parts.Length == 3 && parts[2].ToUpperInvariant() != "CHECKED"))
{
throw new IniParseException($"Invalid syntax for value of {key}! " +
$"Expected path/to/source.file,path/to/destination.file[,checked], read {value}.");
}
bool isChecked = parts.Length == 3 && parts[2].ToUpperInvariant() == "CHECKED";
gameFiles.Add(new(Source: parts[0].Trim(), Target: parts[1].Trim(), isChecked));
}
return gameFiles;
}
public string ExtraExeCommandLineParameters => clientDefinitionsIni.GetStringValue(SETTINGS, "ExtraCommandLineParams", string.Empty);
public string MPMapsIniPath => SafePath.CombineFilePath(clientDefinitionsIni.GetStringValue(SETTINGS, "MPMapsPath", SafePath.CombineFilePath("INI", "MPMaps.ini")));
public string KeyboardINI => clientDefinitionsIni.GetStringValue(SETTINGS, "KeyboardINI", "Keyboard.ini");
public bool SettingsIniAsKeyboardIni => SettingsIniName == KeyboardINI;
public string KeyboardHotkeySection => clientDefinitionsIni.GetStringValue(
SETTINGS,
"KeyboardHotkeySection",
ClientGameType == ClientType.RA ? "WinHotKeys" : "Hotkey");
public int MinimumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumIngameWidth", 640);
public int MinimumIngameHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MinimumIngameHeight", 480);
public int MaximumIngameWidth => clientDefinitionsIni.GetIntValue(SETTINGS, "MaximumIngameWidth", int.MaxValue);
public int MaximumIngameHeight => clientDefinitionsIni.GetIntValue(SETTINGS, "MaximumIngameHeight", int.MaxValue);
public bool CopyMissionsToSpawnmapINI => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CopyMissionsToSpawnmapINI", true);
public string AllowedCustomGameModes => clientDefinitionsIni.GetStringValue(SETTINGS, "AllowedCustomGameModes", "Standard,Custom Map");
public int InactiveHostWarningMessageSeconds => clientDefinitionsIni.GetIntValue(SETTINGS, "InactiveHostWarningMessageSeconds", 0);
public int InactiveHostKickSeconds => clientDefinitionsIni.GetIntValue(SETTINGS, "InactiveHostKickSeconds", 0) + InactiveHostWarningMessageSeconds;
public bool InactiveHostKickEnabled => InactiveHostWarningMessageSeconds > 0 && InactiveHostKickSeconds > 0;
public string SkillLevelOptions => clientDefinitionsIni.GetStringValue(SETTINGS, "SkillLevelOptions", "Any,Beginner,Intermediate,Pro");
public int DefaultSkillLevelIndex => clientDefinitionsIni.GetIntValue(SETTINGS, "DefaultSkillLevelIndex", 0);
public bool CampaignTagSelectorEnabled => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CampaignTagSelectorEnabled", false);
public bool ReturnToMainMenuOnMissionLaunch => clientDefinitionsIni.GetBooleanValue(SETTINGS, "ReturnToMainMenuOnMissionLaunch", true);
public string GetGameExecutableName()
{
string[] exeNames = clientDefinitionsIni.GetStringListValue(SETTINGS, "GameExecutableNames", "Game.exe");
return exeNames[0];
}
public string GameLauncherExecutableName => clientDefinitionsIni.GetStringValue(SETTINGS, "GameLauncherExecutableName", string.Empty);
public string[] GetCompatibilityCheckExecutables()
{
string[] exeNames = clientDefinitionsIni.GetStringListValue(SETTINGS, "CompatibilityCheckExecutables", string.Empty);
return exeNames;
}
public bool SaveSkirmishGameOptions => clientDefinitionsIni.GetBooleanValue(SETTINGS, "SaveSkirmishGameOptions", false);
public bool SaveCampaignGameOptions => clientDefinitionsIni.GetBooleanValue(SETTINGS, "SaveCampaignGameOptions", false);
public bool CreateSavedGamesDirectory => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CreateSavedGamesDirectory", false);
public bool DisableMultiplayerGameLoading => clientDefinitionsIni.GetBooleanValue(SETTINGS, "DisableMultiplayerGameLoading", false);
public bool DisplayPlayerCountInTopBar => clientDefinitionsIni.GetBooleanValue(SETTINGS, "DisplayPlayerCountInTopBar", false);
///
/// The name of the executable in the main game directory that selects
/// the correct main client executable.
/// For example, DTA.exe in case of DTA.
///
public string LauncherExe => clientDefinitionsIni.GetStringValue(SETTINGS, "LauncherExe", string.Empty);
public bool UseClientRandomStartLocations => clientDefinitionsIni.GetBooleanValue(SETTINGS, "UseClientRandomStartLocations", false);
///
/// Returns the name of the game executable file that is used on
/// Linux and macOS.
///
public string UnixGameExecutableName => clientDefinitionsIni.GetStringValue(SETTINGS, "UnixGameExecutableName", "wine-dta.sh");
///
/// List of files that are not distributed but required to play.
///
public string[] RequiredFiles => clientDefinitionsIni.GetStringListValue(SETTINGS, "RequiredFiles", String.Empty);
///
/// List of files that can interfere with the mod functioning.
///
public string[] ForbiddenFiles => clientDefinitionsIni.GetStringListValue(SETTINGS, "ForbiddenFiles", String.Empty);
///
/// The main map file extension that is read by the client.
///
public string MapFileExtension => clientDefinitionsIni.GetStringValue(SETTINGS, "MapFileExtension", "map");
///
/// This tells the client which supplemental map files are ok to copy over during "spawnmap.ini" file creation.
/// IE, if "BIN" is listed, then the client will look for and copy the file "map_a.bin"
/// when writing the spawnmap.ini file for map file "map_a.ini".
///
/// This configuration should be in the form "SupplementalMapFileExtensions=bin,mix"
///
public IEnumerable SupplementalMapFileExtensions
=> clientDefinitionsIni.GetStringValue(SETTINGS, "SupplementalMapFileExtensions", null)?
.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty();
///
/// This prevents users from joining games that are incompatible/on a different game version than the current user.
/// Default: false
///
public bool DisallowJoiningIncompatibleGames => clientDefinitionsIni.GetBooleanValue(SETTINGS, nameof(DisallowJoiningIncompatibleGames), false);
///
/// Activates warnings for development builds of XNA Client
///
public bool ShowDevelopmentBuildWarnings => clientDefinitionsIni.GetBooleanValue(SETTINGS, nameof(ShowDevelopmentBuildWarnings), true);
#endregion
#region Network definitions
public string CnCNetTunnelListURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetTunnelListURL", "https://cncnet.org/master-list");
public string CnCNetPlayerCountURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetPlayerCountURL", "https://api.cncnet.org/status");
public string CnCNetMapDBDownloadURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetMapDBDownloadURL", "https://mapdb.cncnet.org");
public string CnCNetMapDBUploadURL => networkDefinitionsIni.GetStringValue(SETTINGS, "CnCNetMapDBUploadURL", "https://mapdb.cncnet.org/upload");
public bool DisableDiscordIntegration => networkDefinitionsIni.GetBooleanValue(SETTINGS, "DisableDiscordIntegration", false);
public List IRCServers => GetIRCServers();
#endregion
#region User default settings
public bool UserDefault_BorderlessWindowedClient => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, "BorderlessWindowedClient", true);
public bool UserDefault_IntegerScaledClient => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, "IntegerScaledClient", false);
public bool UserDefault_WriteInstallationPathToRegistry => clientDefinitionsIni.GetBooleanValue(USER_DEFAULTS, "WriteInstallationPathToRegistry", true);
#endregion
#region Game networking defaults
///
/// Default value for FrameSendRate setting written in spawn.ini.
///
public int DefaultFrameSendRate => clientDefinitionsIni.GetIntValue(SETTINGS, nameof(DefaultFrameSendRate), 7);
///
/// Default value for Protocol setting written in spawn.ini.
///
public int DefaultProtocolVersion => clientDefinitionsIni.GetIntValue(SETTINGS, nameof(DefaultProtocolVersion), 2);
///
/// Default value for MaxAhead setting written in spawn.ini.
///
public int DefaultMaxAhead => clientDefinitionsIni.GetIntValue(SETTINGS, nameof(DefaultMaxAhead), 0);
#endregion
public List GetIRCServers()
{
List servers = [];
IniSection serversSection = networkDefinitionsIni.GetSection("IRCServers");
if (serversSection != null)
foreach ((_, string value) in serversSection.Keys)
if (!string.IsNullOrWhiteSpace(value))
servers.Add(value);
return servers;
}
public bool DiscordIntegrationGloballyDisabled => string.IsNullOrWhiteSpace(DiscordAppId) || DisableDiscordIntegration;
public string CustomMissionPath => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionPath", "Maps/CustomMissions");
public List<(string extension, string copyAs)> GetCustomMissionSupplementFiles()
{
List<(string extension, string copyAs)> files = new();
Dictionary extensionToIndex = new(StringComparer.OrdinalIgnoreCase);
int index = 0;
while (true)
{
string extensionKey = $"CustomMissionSupplementFile{index}Extension";
string copyAsKey = $"CustomMissionSupplementFile{index}CopyAs";
string extension = clientDefinitionsIni.GetStringValue(SETTINGS, extensionKey, null)?.Trim();
// Stop iteration if the extension key is missing
if (string.IsNullOrWhiteSpace(extension))
break;
string copyAs = clientDefinitionsIni.GetStringValue(SETTINGS, copyAsKey, null);
// Validate that copyAs is not empty
if (string.IsNullOrWhiteSpace(copyAs))
throw new ClientConfigurationException($"Configuration key '{copyAsKey}' is required when '{extensionKey}' is present for supplement file {index}.");
// Validate that extension is unique
if (extensionToIndex.TryGetValue(extension, out int firstIndex))
throw new ClientConfigurationException($"Duplicate extension '{extension}' found in supplement files. Extension is used in both file {firstIndex} and file {index}.");
extensionToIndex.Add(extension, index);
files.Add((extension, copyAs));
index++;
}
return files;
}
public OSVersion GetOperatingSystemVersion()
{
#if NETFRAMEWORK
// OperatingSystem.IsWindowsVersionAtLeast() is the preferred API but is not supported on earlier .NET versions
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
Version osVersion = Environment.OSVersion.Version;
if (osVersion.Major <= 4)
return OSVersion.UNKNOWN;
if (osVersion.Major == 5)
return OSVersion.WINXP;
if (osVersion.Major == 6 && osVersion.Minor == 0)
return OSVersion.WINVISTA;
if (osVersion.Major == 6 && osVersion.Minor <= 1)
return OSVersion.WIN7;
return OSVersion.WIN810;
}
if (ProgramConstants.ISMONO)
return OSVersion.UNIX;
// http://mono.wikia.com/wiki/Detecting_the_execution_platform
int p = (int)Environment.OSVersion.Platform;
if (p == 4 || p == 6 || p == 128)
return OSVersion.UNIX;
return OSVersion.UNKNOWN;
#else
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2))
return OSVersion.WIN810;
else if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))
return OSVersion.WIN7;
else if (OperatingSystem.IsWindowsVersionAtLeast(6, 0))
return OSVersion.WINVISTA;
else if (OperatingSystem.IsWindowsVersionAtLeast(5, 0))
return OSVersion.WINXP;
else
return OSVersion.UNKNOWN;
}
return OSVersion.UNIX;
#endif
}
}
///
/// An exception that is thrown when a client configuration file contains invalid or
/// unexpected settings / data or required settings / data are missing.
///
public class ClientConfigurationException : Exception
{
public ClientConfigurationException(string message) : base(message)
{
}
}
}
================================================
FILE: ClientCore/ClientCore.csproj
================================================
CnCNet Client Core Library
================================================
FILE: ClientCore/Enums/AllowPrivateMessagesFromEnum.cs
================================================
namespace ClientCore.Enums
{
public enum AllowPrivateMessagesFromEnum
{
All = 1,
CurrentChannel = 4,
Friends = 2,
None = 3,
}
}
================================================
FILE: ClientCore/Enums/ClientType.cs
================================================
namespace ClientCore.Enums
{
public enum ClientType
{
TS,
YR,
Ares,
RA,
}
}
================================================
FILE: ClientCore/Enums/ClientTypeHelper.cs
================================================
using System;
using System.Linq;
using ClientCore.Extensions;
namespace ClientCore.Enums
{
public static class ClientTypeHelper
{
public static ClientType FromString(string value) => value switch
{
"TS" => ClientType.TS,
"YR" => ClientType.YR,
"Ares" => ClientType.Ares,
"RA" => ClientType.RA,
_ => throw new Exception(string.Format((
"It seems the client configuration was not migrated to accommodate for the v2.12 changes. " +
"Please specify 'ClientGameType' in '[Settings]' section of the 'ClientDefinitions.ini' file " +
"(allowed options: {0}).\n\n" +
"Please refer to documentation of the client {1} for more details. This link can also be found in the log file.").L10N("Client:Main:ClientGameTypeNotFoundException"),
EnumExtensions.GetNames(),
"https://github.com/CnCNet/xna-cncnet-client/")),
};
}
}
================================================
FILE: ClientCore/Enums/SortDirection.cs
================================================
namespace ClientCore.Enums
{
public enum SortDirection
{
None = 0,
Asc = 1,
Desc = 2
}
}
================================================
FILE: ClientCore/Extensions/ArrayExtensions.cs
================================================
using System;
namespace ClientCore.Extensions;
// https://stackoverflow.com/a/65894979/20766970
public static class ArrayExtensions
{
public static void Deconstruct(this T[] @this, out T a0)
{
if (@this == null || @this.Length < 1)
throw new ArgumentException(null, nameof(@this));
a0 = @this[0];
}
public static (T, T) AsTuple2(this T[] @this)
{
if (@this == null || @this.Length < 2)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1)
=> (a0, a1) = @this.AsTuple2();
public static (T, T, T) AsTuple3(this T[] @this)
{
if (@this == null || @this.Length < 3)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1], @this[2]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1, out T a2)
=> (a0, a1, a2) = @this.AsTuple3();
public static (T, T, T, T) AsTuple4(this T[] @this)
{
if (@this == null || @this.Length < 4)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1], @this[2], @this[3]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1, out T a2, out T a3)
=> (a0, a1, a2, a3) = @this.AsTuple4();
public static (T, T, T, T, T) AsTuple5(this T[] @this)
{
if (@this == null || @this.Length < 5)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1], @this[2], @this[3], @this[4]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4)
=> (a0, a1, a2, a3, a4) = @this.AsTuple5();
public static (T, T, T, T, T, T) AsTuple6(this T[] @this)
{
if (@this == null || @this.Length < 6)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1], @this[2], @this[3], @this[4], @this[5]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4, out T a5)
=> (a0, a1, a2, a3, a4, a5) = @this.AsTuple6();
public static (T, T, T, T, T, T, T) AsTuple7(this T[] @this)
{
if (@this == null || @this.Length < 7)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1], @this[2], @this[3], @this[4], @this[5], @this[6]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4, out T a5, out T a6)
=> (a0, a1, a2, a3, a4, a5, a6) = @this.AsTuple7();
public static (T, T, T, T, T, T, T, T) AsTuple8(this T[] @this)
{
if (@this == null || @this.Length < 8)
throw new ArgumentException(null, nameof(@this));
return (@this[0], @this[1], @this[2], @this[3], @this[4], @this[5], @this[6], @this[7]);
}
public static void Deconstruct(this T[] @this, out T a0, out T a1, out T a2, out T a3, out T a4, out T a5, out T a6, out T a7)
=> (a0, a1, a2, a3, a4, a5, a6, a7) = @this.AsTuple8();
}
================================================
FILE: ClientCore/Extensions/EnumExtensions.cs
================================================
using System;
using System.Linq;
using System.Text;
namespace ClientCore.Extensions
{
public static class EnumExtensions
{
public static T CycleNext(this T src) where T : Enum
{
T[] values = EnumExtensions.GetValues();
return values[(Array.IndexOf(values, src) + 1) % values.Length];
}
public static T First() where T : Enum
=> EnumExtensions.GetValues()[0];
public static string GetNames() where T : Enum
=> string.Join(", ", EnumExtensions.GetValues().Select(e => e.ToString()));
private static T[] GetValues() where T : Enum
=> (T[])Enum.GetValues(typeof(T));
}
}
================================================
FILE: ClientCore/Extensions/EnumerableExtensions.cs
================================================
using System.Collections.Generic;
using System.Linq;
namespace ClientCore.Extensions;
public static class EnumerableExtensions
{
///
/// Converts an enumerable to a matrix of items with a max number of items per column.
/// The matrix is built column by column, left to right.
///
/// the enumerable to convert
/// the max number of items per column
///
///
public static List> ToMatrix(this IEnumerable enumerable, int maxPerColumn)
{
var list = enumerable.ToList();
return list.Aggregate(new List>(), (matrix, item) =>
{
int index = list.IndexOf(item);
int column = (index / maxPerColumn);
List columnList = matrix.Count <= column ? new List() : matrix[column];
if (columnList.Count == 0)
matrix.Add(columnList);
columnList.Add(item);
return matrix;
});
}
}
================================================
FILE: ClientCore/Extensions/FileExtensions.cs
================================================
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Rampastring.Tools;
using ClientCore.PlatformShim;
namespace ClientCore.Extensions;
public class FileExtensions
{
///
/// Establishes a hard link between an existing file and a new file. This function is only supported on the NTFS file system, and only for files, not directories.
///
/// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw
///
/// The name of the new file.
/// The name of the existing file.
/// Reserved; must be NULL.
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero (0).
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateHardLinkW")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[SupportedOSPlatform("windows")]
private static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
///
/// The link function makes a new link to the existing file named by oldname, under the new name newname.
///
/// https://www.gnu.org/software/libc/manual/html_node/Hard-Links.html
///
///
/// This function returns a value of 0 if it is successful and -1 on failure.
[DllImport("libc", EntryPoint = "link", SetLastError = true)]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("osx")]
private static extern int link([MarshalAs(UnmanagedType.LPUTF8Str)] string oldname, [MarshalAs(UnmanagedType.LPUTF8Str)] string newname);
///
/// Creates hard link to the source file or copy that file, if got an error.
///
///
///
///
///
///
public static void CreateHardLinkFromSource(string source, string destination, bool fallback = true)
{
if (fallback)
{
try
{
CreateHardLinkFromSource(source, destination, fallback: false);
}
catch (Exception ex)
{
Logger.Log($"Failed to create hard link at {destination}. Fallback to copy. {ex.Message}");
File.Copy(source, destination, true);
}
return;
}
if (File.Exists(destination))
{
FileInfo destinationFile = new(destination);
destinationFile.IsReadOnly = false;
destinationFile.Delete();
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!CreateHardLink(destination, source, IntPtr.Zero))
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (link(source, destination) != 0)
throw new IOException(string.Format("Unable to create hard link at {0} with the following error code: {1}"
.L10N("Client:DTAConfig:CreateHardLinkFailed"), destination, Marshal.GetLastWin32Error()));
}
else
{
throw new PlatformNotSupportedException();
}
}
///
/// Predicts text file encoding by its content.
///
///
///
///
public static Encoding GetDetectedEncoding(string filename, float minimalConfidence = 0.5f)
{
Encoding encoding = EncodingExt.UTF8NoBOM;
using (FileStream fs = File.OpenRead(filename))
{
Ude.CharsetDetector cdet = new Ude.CharsetDetector();
cdet.Feed(fs);
cdet.DataEnd();
if (cdet.Charset != null && cdet.Confidence > minimalConfidence)
{
Encoding detectedEncoding = Encoding.GetEncoding(cdet.Charset);
if (detectedEncoding is not UTF8Encoding and not ASCIIEncoding)
encoding = detectedEncoding;
}
}
return encoding;
}
}
================================================
FILE: ClientCore/Extensions/IniFileExtensions.cs
================================================
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Rampastring.Tools;
namespace ClientCore.Extensions
{
public static class IniFileExtensions
{
extension(IniFile iniFile)
{
// Clone() method is not officially available now. https://github.com/Rampastring/Rampastring.Tools/issues/12
public IniFile Clone()
{
var newIni = new IniFile();
foreach (string sectionName in iniFile.GetSections())
{
IniSection oldSection = iniFile.GetSection(sectionName);
newIni.AddSection(oldSection.Clone());
}
return newIni;
}
public IniSection GetOrAddSection(string sectionName)
{
var section = iniFile.GetSection(sectionName);
if (section != null)
return section;
section = new IniSection(sectionName);
iniFile.AddSection(section);
return section;
}
public string[] GetStringListValue(string section, string key, string defaultValue, char[]? separators = null)
=> (iniFile.GetSection(section)?.GetStringValue(key, defaultValue) ?? defaultValue)
.SplitWithCleanup(separators);
}
extension(IniSection iniSection)
{
public IniSection Clone()
{
IniSection newSection = new(iniSection.SectionName);
foreach ((var key, var value) in iniSection.Keys)
{
newSection.AddKey(key, value);
}
return newSection;
}
public void RemoveAllKeys()
{
var keys = new List>(iniSection.Keys);
foreach (KeyValuePair iniSectionKey in keys)
iniSection.RemoveKey(iniSectionKey.Key);
}
public string? GetStringValueOrNull(string key) =>
iniSection.KeyExists(key) ? iniSection.GetStringValue(key, string.Empty) : null;
public int? GetIntValueOrNull(string key) =>
iniSection.KeyExists(key) ? iniSection.GetIntValue(key, 0) : null;
public bool? GetBooleanValueOrNull(string key) =>
iniSection.KeyExists(key) ? iniSection.GetBooleanValue(key, false) : null;
public List? GetListValueOrNull(string key, char separator, Func converter) =>
iniSection.KeyExists(key) ? iniSection.GetListValue(key, separator, converter) : null;
}
}
}
================================================
FILE: ClientCore/Extensions/StringExtensions.cs
================================================
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using ClientCore.I18N;
namespace ClientCore.Extensions;
public static class StringExtensions
{
private static Regex extractLinksRE = new Regex(@"((http[s]?)|(ftp))\S+");
public static string[] GetLinks(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return null;
var matches = extractLinksRE.Matches(text);
if (matches.Count == 0)
return null; // No link found
string[] links = new string[matches.Count];
for (int i = 0; i < links.Length; i++)
links[i] = matches[i].Value.Trim();
return links;
}
private const string ESCAPED_INI_NEWLINE_PATTERN = $"\\{ProgramConstants.INI_NEWLINE_PATTERN}";
private const string ESCAPED_SEMICOLON = "\\semicolon";
///
/// Converts a regular string to an INI representation of it.
///
/// Input string.
/// INI-safe string.
public static string ToIniString(this string raw)
{
if (raw.Contains(ESCAPED_INI_NEWLINE_PATTERN, StringComparison.InvariantCulture))
throw new ArgumentException($"The string contains an illegal character sequence! ({ESCAPED_INI_NEWLINE_PATTERN})");
if (raw.Contains(ESCAPED_SEMICOLON, StringComparison.InvariantCulture))
throw new ArgumentException($"The string contains an illegal character sequence! ({ESCAPED_SEMICOLON})");
return raw
.Replace(ProgramConstants.INI_NEWLINE_PATTERN, ESCAPED_INI_NEWLINE_PATTERN)
.Replace(";", ESCAPED_SEMICOLON)
.Replace(Environment.NewLine, "\n")
.Replace("\n", ProgramConstants.INI_NEWLINE_PATTERN);
}
///
/// Converts an INI-safe string to a normal string.
///
/// Input INI string.
/// Regular string.
public static string FromIniString(this string iniString)
{
return iniString
.Replace(ESCAPED_INI_NEWLINE_PATTERN, ProgramConstants.INI_NEWLINE_PATTERN)
.Replace(ESCAPED_SEMICOLON, ";")
.Replace(ProgramConstants.INI_NEWLINE_PATTERN, Environment.NewLine);
}
///
/// Looks up a translated string for the specified key.
///
/// The default string value as a fallback.
/// The unique key name.
/// Whether to add this key and value to the list of missing key-values.
/// The translated string value.
///
/// This method is referenced by TranslationNotifierGenerator in order to check if the const
/// values that are not initialized on client start automatically are missing (via notification
/// mechanism implemented down the call chain). Do not change the signature or move the method out
/// of the namespace it's currently defined in. If you do - you have to also edit the generator
/// source code to match.
///
public static string L10N(this string defaultValue, string key, bool notify = true)
=> string.IsNullOrEmpty(defaultValue)
? defaultValue
: Translation.Instance.LookUp(key, defaultValue, notify);
///
/// Replace special characters with spaces in the filename to avoid conflicts with WIN32API.
///
/// The default string value.
/// File name without special characters or reserved combinations.
///
/// Reference: Naming Files, Paths, and Namespaces.
///
public static string ToWin32FileName(this string filename)
{
foreach (char ch in "/\\:*?<>|")
filename = filename.Replace(ch, '_');
// If the user is somehow using "con" or any other filename that is
// reserved by WIN32API, it would be better to rename it.
HashSet reservedFileNames = new HashSet(new List(){
"CON",
"PRN",
"AUX",
"NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³"
}, StringComparer.InvariantCultureIgnoreCase);
if (reservedFileNames.Contains(filename))
filename += "_";
return filename;
}
public static T ToEnum(this string value) where T : Enum
=> (T)Enum.Parse(typeof(T), value, true);
public static string[] SplitWithCleanup(this string value, char[] separators = null)
=> value
.Split(separators ?? [','])
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
}
================================================
FILE: ClientCore/I18N/Translation.cs
================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using ClientCore.Extensions;
using ClientCore.PlatformShim;
using Rampastring.Tools;
namespace ClientCore.I18N;
public class Translation : ICloneable
{
public static Translation Instance { get; set; } = new Translation(ProgramConstants.HARDCODED_LOCALE_CODE);
/// The translation metadata section name.
public const string METADATA_SECTION = "General";
private static CultureInfo _initialUICulture;
/// The UI culture that the application was started with. Must be initialized as early as possible.
public static CultureInfo InitialUICulture
{
get => _initialUICulture;
set => _initialUICulture = _initialUICulture is null ? value
: throw new InvalidOperationException($"{nameof(InitialUICulture)} is already set!");
}
/// AKA name of the folder, used to look up and select a language
public string LocaleCode { get; private set; } = string.Empty;
/// The explicitly set UI name for the translation.
private string _name = string.Empty;
/// The UI name for the translation.
public string Name
{
get => string.IsNullOrWhiteSpace(_name) ? GetLanguageName(LocaleCode) : _name;
private set => _name = value;
}
/// The explicitly set UI culture for the translation.
/// Not accounted when selecting the translation automatically.
private CultureInfo _culture;
/// The UI culture for the translation.
public CultureInfo Culture
{
get => _culture is null ? new CultureInfo(LocaleCode) : _culture;
private set => _culture = value;
}
/// The author(s) of the translation.
public string Author { get; private set; } = string.Empty;
/// Override the default encoding used for reading/writing map files. Null ("Auto") means detecting the encoding from each file (sometimes unreliable).
public Encoding MapEncoding = EncodingExt.UTF8NoBOM;
/// Stores the translation values (including default values for missing strings).
private Dictionary Values { get; } = new();
// public bool IsRightToLeft { get; set; } // TODO
/// Contains all keys within with missing translations.
private readonly HashSet MissingKeys = new();
/// Used to write missing translation table entries to a file.
public const string MISSING_KEY_PREFIX = "; "; // a hack but hey it works
///
/// Initializes a new instance of the class.
///
/// A locale code for this translation.
public Translation(string localeCode)
{
LocaleCode = localeCode;
}
///
/// Initializes a new instance of the class
/// that loads the translation from an INI file.
///
/// An INI file to read from.
/// A locale code for this translation.
public Translation(IniFile ini, string localeCode)
: this(localeCode)
{
if (ini is null)
throw new ArgumentNullException(nameof(ini));
IniSection metadataSection = ini.GetSection(METADATA_SECTION);
Name = metadataSection?.GetStringValue(nameof(Name), string.Empty);
Author = metadataSection?.GetStringValue(nameof(Author), string.Empty);
MapEncoding = EncodingExt.GetEncodingWithAuto(metadataSection?.GetStringValue(nameof(MapEncoding), null));
string cultureName = metadataSection?.GetStringValue(nameof(Culture), null);
if (cultureName is not null)
Culture = new(cultureName);
AppendValuesFromIniFile(ini);
}
///
/// Initializes a new instance of the class
/// that loads the translation from an INI file.
///
/// A path to an INI file to read from.
/// A locale code for this translation.
public Translation(string iniPath, string localeCode)
: this(new CCIniFile(iniPath), localeCode) { }
///
/// Initializes a new instance of the class
/// that is a copy of the given instance.
///
/// An object to copy from.
public Translation(Translation other)
{
LocaleCode = other.LocaleCode;
_name = other._name;
_culture = other._culture;
Author = other.Author;
MapEncoding = other.MapEncoding;
foreach (var (key, value) in other.Values)
Values.Add(key, value);
}
public Translation Clone() => new Translation(this);
object ICloneable.Clone() => Clone();
///
/// Reads from an INI file, overriding possibly existing ones.
///
/// A path to an INI file to read from.
public void AppendValuesFromIniFile(string iniPath)
=> AppendValuesFromIniFile(new CCIniFile(iniPath));
///
/// Reads from an INI file, overriding possibly existing ones.
///
/// An INI file to read from.
public void AppendValuesFromIniFile(IniFile ini)
{
IniSection valuesSection = ini.GetSection(nameof(Values));
foreach (var (key, value) in valuesSection.Keys)
Values[key] = value.FromIniString();
}
/// The locale code to look up the language name for.
/// The language name for the given locale code.
public static string GetLanguageName(string localeCode)
{
string result = null;
string iniPath = SafePath.CombineFilePath(
ClientConfiguration.Instance.TranslationsFolderPath, localeCode, ClientConfiguration.Instance.TranslationIniName);
if (SafePath.GetFile(iniPath).Exists)
{
// This parses only the metadata section content so that we don't parse
// the bazillion of localized values just to read the translation name.
// The only issue is that inheritance would break.
// FIXME AllowNewSections is ignored with inheritance
IniFile ini = new();
ini.AddSection(METADATA_SECTION);
ini.FileName = iniPath;
ini.AllowNewSections = false;
ini.Parse();
// Overridden name first
IniSection metadataSection = ini.GetSection(METADATA_SECTION);
result = metadataSection?.GetStringValue(nameof(Name), null);
}
if (string.IsNullOrWhiteSpace(result))
result = new CultureInfo(localeCode).NativeName;
if (string.IsNullOrWhiteSpace(result))
result = localeCode;
return result;
}
///
/// Applies (hard-links or copies) the translation game files for a given locale to the game directory,
/// and removes any destination files whose source no longer exists.
///
public void ApplyTranslationGameFiles() => ApplyTranslationGameFiles(LocaleCode);
///
/// The locale code identifying the translation whose game files should be applied.
public static void ApplyTranslationGameFiles(string localeCode)
{
ClientConfiguration.Instance.RefreshTranslationGameFiles();
string translationFolderPath = SafePath.CombineDirectoryPath(
ClientConfiguration.Instance.TranslationsFolderPath, localeCode);
foreach (var tgf in ClientConfiguration.Instance.TranslationGameFiles)
{
string sourcePath = SafePath.CombineFilePath(translationFolderPath, tgf.Source);
string targetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, tgf.Target);
if (File.Exists(sourcePath))
{
string sourceHash = Utilities.CalculateSHA1ForFile(sourcePath);
string targetHash = Utilities.CalculateSHA1ForFile(targetPath);
if (sourceHash != targetHash)
{
FileExtensions.CreateHardLinkFromSource(sourcePath, targetPath);
new FileInfo(targetPath).IsReadOnly = true;
}
}
else
{
if (File.Exists(targetPath))
{
new FileInfo(targetPath).IsReadOnly = false;
File.Delete(targetPath);
}
}
}
}
///
/// Lists valid available translations from the along with their UI names.
/// A localization is valid if it has a corresponding file in the .
///
/// Locale code -> display name pairs.
public static Dictionary GetTranslations()
{
var translations = new Dictionary(StringComparer.InvariantCultureIgnoreCase)
{
// Add default localization so that we always have it in the list even if the localization does not exist
[ProgramConstants.HARDCODED_LOCALE_CODE] = GetLanguageName(ProgramConstants.HARDCODED_LOCALE_CODE)
};
if (!Directory.Exists(ClientConfiguration.Instance.TranslationsFolderPath))
return translations;
foreach (var localizationFolder in Directory.GetDirectories(ClientConfiguration.Instance.TranslationsFolderPath))
{
string localizationCode = Path.GetFileName(localizationFolder);
translations[localizationCode] = GetLanguageName(localizationCode);
}
return translations;
}
///
/// Checks the current UI culture and finds the closest match from supported translations.
///
/// Available translation locale code.
public static string GetDefaultTranslationLocaleCode()
{
// we don't need names here pretty much
Dictionary translations = GetTranslations();
for (var culture = InitialUICulture;
culture != CultureInfo.InvariantCulture;
culture = culture.Parent)
{
string translation = culture.Name;
// the keys in 'translations' are case-insensitive
if (translations.ContainsKey(translation))
return translation;
}
return ProgramConstants.HARDCODED_LOCALE_CODE;
}
///
/// Dump the translation table to an ini file.
///
/// An ini file that contains the translation table.
public IniFile DumpIni(bool saveOnlyMissingValues = false)
{
IniFile ini = new IniFile();
ini.AddSection(METADATA_SECTION);
IniSection general = ini.GetSection(METADATA_SECTION);
if (!string.IsNullOrWhiteSpace(_name))
general.AddKey(nameof(Name), _name);
if (_culture is not null)
general.AddKey(nameof(Culture), _culture.Name);
general.AddKey(nameof(Author), Author);
general.AddKey(nameof(MapEncoding), EncodingExt.EncodingWithAutoToString(MapEncoding));
ini.AddSection(nameof(Values));
IniSection translation = ini.GetSection(nameof(Values));
foreach (var (key, value) in Values.OrderBy(kvp => kvp.Key))
{
bool valueMissing = MissingKeys.Contains(key);
if (!saveOnlyMissingValues || valueMissing)
{
translation.AddKey(valueMissing
? MISSING_KEY_PREFIX + key
: key,
value.ToIniString());
}
}
return ini;
}
private bool HandleMissing(string key, string defaultValue)
{
if (MissingKeys.Add(key))
{
Values[key] = defaultValue;
return true;
}
return false;
}
///
/// Looks up the translated value that corresponds to the given key.
///
/// The translation key (identifier).
/// The value to fall back to in case there's no translated value.
/// Whether to add this key and value to the list of missing key-values.
/// The translated value or a default value.
public string LookUp(string key, string defaultValue, bool notify = true)
{
if (Values.ContainsKey(key))
return Values[key];
if (notify)
_ = HandleMissing(key, defaultValue);
return defaultValue;
}
///
/// Looks up the translated value that corresponds to the given key with a fallback to the value of global key.
///
/// The translation key (identifier).
/// The fallback translation key (identifier).
/// The value to fall back to in case there's no translated value.
/// Whether to add this key and value to the list of missing key-values. Doesn't include the fallback key.
/// The translated value or a default value.
public string LookUp(string key, string fallbackKey, string defaultValue, bool notify = true)
{
string result;
if (Values.ContainsKey(key))
{
result = Values[key];
}
else if (key != fallbackKey && Values.ContainsKey(fallbackKey))
{
result = Values[fallbackKey];
}
else
{
result = defaultValue;
if (notify)
_ = HandleMissing(key, defaultValue);
}
return result;
}
}
================================================
FILE: ClientCore/I18N/TranslationGameFile.cs
================================================
namespace ClientCore.I18N;
///
/// Describes a file to try and copy into the game folder with a translation.
///
/// A path to copy from, relative to the selected translation folder.
/// A path to copy to, relative to root folder of the game/mod.
/// Whether to include this file in the integrity checks.
public readonly record struct TranslationGameFile(string Source, string Target, bool Checked);
================================================
FILE: ClientCore/INIProcessing/IniPreprocessInfoStore.cs
================================================
using Rampastring.Tools;
using System;
using System.Collections.Generic;
using System.IO;
using ClientCore.Extensions;
namespace ClientCore.INIProcessing
{
public class PreprocessedIniInfo
{
public PreprocessedIniInfo(string fileName, string originalHash, string processedHash)
{
FileName = fileName;
OriginalFileHash = originalHash;
ProcessedFileHash = processedHash;
}
public PreprocessedIniInfo(string[] info)
{
FileName = info[0];
OriginalFileHash = info[1];
ProcessedFileHash = info[2];
}
public string FileName { get; }
public string OriginalFileHash { get; set; }
public string ProcessedFileHash { get; set; }
}
///
/// Handles information on what INI files have been processed by the client.
///
public class IniPreprocessInfoStore
{
private const string StoreIniName = "ProcessedIniInfo.ini";
private const string ProcessedINIsSection = "ProcessedINIs";
public List PreprocessedIniInfos { get; } = new List();
///
/// Loads the preprocessed INI information.
///
public void Load()
{
FileInfo processedIniInfoFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "ProcessedIniInfo.ini");
if (!processedIniInfoFile.Exists)
return;
var iniFile = new IniFile(processedIniInfoFile.FullName);
var keys = iniFile.GetSectionKeys(ProcessedINIsSection);
foreach (string key in keys)
{
string[] values = iniFile.GetStringListValue(ProcessedINIsSection, key, string.Empty);
if (values.Length != 3)
{
Logger.Log("Failed to parse preprocessed INI info, key " + key);
continue;
}
// If an INI file no longer exists, it's useless to keep its record
if (!SafePath.GetFile(ProgramConstants.GamePath, "INI", values[0]).Exists)
continue;
PreprocessedIniInfos.Add(new PreprocessedIniInfo(values));
}
}
///
/// Checks if a (potentially processed) INI file is up-to-date
/// or whether it needs to be (re)processed.
///
/// The name of the INI file in its directory.
/// Do not supply the entire file path.
/// True if the INI file is up-to-date, false if it needs to be processed.
public bool IsIniUpToDate(string fileName)
{
PreprocessedIniInfo info = PreprocessedIniInfos.Find(i => i.FileName == fileName);
if (info == null)
return false;
string processedFileHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", fileName));
if (processedFileHash != info.ProcessedFileHash)
return false;
string originalFileHash = Utilities.CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Base", fileName));
if (originalFileHash != info.OriginalFileHash)
return false;
return true;
}
public void UpsertRecord(string fileName, string originalFileHash, string processedFileHash)
{
var existing = PreprocessedIniInfos.Find(i => i.FileName == fileName);
if (existing == null)
{
PreprocessedIniInfos.Add(new PreprocessedIniInfo(fileName, originalFileHash, processedFileHash));
}
else
{
existing.OriginalFileHash = originalFileHash;
existing.ProcessedFileHash = processedFileHash;
}
}
public void Write()
{
FileInfo processedIniInfoFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "ProcessedIniInfo.ini");
if (processedIniInfoFile.Exists)
processedIniInfoFile.Delete();
IniFile iniFile = new IniFile(processedIniInfoFile.FullName);
for (int i = 0; i < PreprocessedIniInfos.Count; i++)
{
PreprocessedIniInfo info = PreprocessedIniInfos[i];
iniFile.SetStringValue(ProcessedINIsSection, i.ToString(),
string.Join(",", info.FileName, info.OriginalFileHash, info.ProcessedFileHash));
}
iniFile.WriteIniFile();
}
}
}
================================================
FILE: ClientCore/INIProcessing/IniPreprocessor.cs
================================================
using Rampastring.Tools;
using System.Collections.Generic;
using System.IO;
namespace ClientCore.INIProcessing
{
///
/// Pre-processes INI files.
/// Allows sections to inherit other sections.
///
public class IniPreprocessor
{
public void ProcessIni(string sourceIniPath, string destinationIniPath)
{
File.Delete(destinationIniPath);
if (!File.Exists(sourceIniPath))
return;
var iniFile = new IniFile(sourceIniPath);
List sections = iniFile.GetSections();
sections.ForEach(sectionName => ProcessSection(iniFile, sectionName));
iniFile.Comment = $"generated by CnCNet client, see /Base/{Path.GetFileName(sourceIniPath)} for the original";
iniFile.WriteIniFile(destinationIniPath);
}
///
/// Processes an INI section and applies its potential base section.
/// Returns the INI section. Works recursively.
///
/// The INI file.
/// The name of the section to process.
private IniSection ProcessSection(IniFile iniFile, string sectionName)
{
IniSection section = iniFile.GetSection(sectionName);
if (section == null)
return null;
string baseSectionName = section.GetStringValue("BaseSection", string.Empty);
if (string.IsNullOrWhiteSpace(baseSectionName))
return section;
IniSection baseSection = ProcessSection(iniFile, baseSectionName);
if (baseSection == null)
return section;
foreach (var kvp in baseSection.Keys)
{
if (!section.KeyExists(kvp.Key))
section.AddKey(kvp.Key, kvp.Value);
}
return section;
}
}
}
================================================
FILE: ClientCore/INIProcessing/PreprocessorBackgroundTask.cs
================================================
using Rampastring.Tools;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace ClientCore.INIProcessing
{
///
/// Background task for pre-processing INI files.
/// Singleton.
///
public class PreprocessorBackgroundTask
{
private PreprocessorBackgroundTask()
{
}
private static PreprocessorBackgroundTask _instance;
public static PreprocessorBackgroundTask Instance
{
get
{
if (_instance == null)
_instance = new PreprocessorBackgroundTask();
return _instance;
}
}
private Task task;
public bool IsRunning => !task.IsCompleted;
public void Run()
{
task = Task.Run(CheckFiles);
}
private static void CheckFiles()
{
Logger.Log("Starting background processing of INI files.");
DirectoryInfo iniFolder = SafePath.GetDirectory(ProgramConstants.GamePath, "INI", "Base");
if (!iniFolder.Exists)
{
Logger.Log("/INI/Base does not exist, skipping background processing of INI files.");
return;
}
IniPreprocessInfoStore infoStore = new IniPreprocessInfoStore();
infoStore.Load();
IniPreprocessor processor = new IniPreprocessor();
IEnumerable iniFiles = iniFolder.EnumerateFiles("*.ini", SearchOption.TopDirectoryOnly);
int processedCount = 0;
foreach (FileInfo iniFile in iniFiles)
{
if (!infoStore.IsIniUpToDate(iniFile.Name))
{
Logger.Log("INI file " + iniFile.Name + " is not processed or outdated, re-processing it.");
string sourcePath = iniFile.FullName;
string destinationPath = SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", iniFile.Name);
processor.ProcessIni(sourcePath, destinationPath);
string sourceHash = Utilities.CalculateSHA1ForFile(sourcePath);
string destinationHash = Utilities.CalculateSHA1ForFile(destinationPath);
infoStore.UpsertRecord(iniFile.Name, sourceHash, destinationHash);
processedCount++;
}
else
{
Logger.Log("INI file " + iniFile.Name + " is up to date.");
}
}
if (processedCount > 0)
{
Logger.Log("Writing preprocessed INI info store.");
infoStore.Write();
}
Logger.Log("Ended background processing of INI files.");
}
}
}
================================================
FILE: ClientCore/LoadingScreenController.cs
================================================
using System;
using Rampastring.Tools;
namespace ClientCore
{
public static class LoadingScreenController
{
public static string GetLoadScreenName(string sideId)
{
int resHeight = UserINISettings.Instance.IngameScreenHeight;
int randomInt = new Random().Next(1, 1 + ClientConfiguration.Instance.LoadingScreenCount);
string resolutionText;
if (resHeight < 480)
resolutionText = "400";
else if (resHeight < 600)
resolutionText = "480";
else
resolutionText = "600";
return SafePath.CombineFilePath(
ProgramConstants.BASE_RESOURCE_PATH,
FormattableString.Invariant($"l{resolutionText}s{sideId}{randomInt}.pcx")).Replace('\\', '/');
}
}
}
================================================
FILE: ClientCore/OSVersion.cs
================================================
public enum OSVersion
{
UNKNOWN,
WINXP,
WINVISTA,
WIN7,
WIN810,
UNIX
}
================================================
FILE: ClientCore/PlatformShim/EncodingExt.cs
================================================
#nullable enable
using System.Text;
namespace ClientCore.PlatformShim;
public static class EncodingExt
{
static EncodingExt()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
ANSI = Encoding.GetEncoding(0);
}
///
/// Gets the legacy ANSI encoding (not Windows-1252 and also not any specific encoding).
/// ANSI doesn't mean a specific codepage, it means the default non-Unicode codepage which can be changed from Control Panel.
///
public static Encoding ANSI { get; }
public static Encoding UTF8NoBOM { get; } = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
public const string ENCODING_AUTO_DETECT = "Auto";
public static Encoding? GetEncodingWithAuto(string? encodingName)
{
if (encodingName is null)
return UTF8NoBOM;
if (encodingName.Equals(ENCODING_AUTO_DETECT, System.StringComparison.InvariantCultureIgnoreCase))
return null;
Encoding encoding = Encoding.GetEncoding(encodingName);
// We don't expect UTF-8 BOM for the string "UTF-8"
if (encoding is UTF8Encoding)
encoding = UTF8NoBOM;
return encoding;
}
public static string EncodingWithAutoToString(Encoding? encoding)
{
if (encoding is null)
return ENCODING_AUTO_DETECT;
// To find a name that can be passed to the GetDetectedEncoding method, use the WebName property.
return encoding.WebName;
}
}
================================================
FILE: ClientCore/ProcessLauncher.cs
================================================
using System.Diagnostics;
namespace ClientCore
{
public static class ProcessLauncher
{
public static void StartShellProcess(string commandLine, string arguments = null)
{
using var _ = Process.Start(new ProcessStartInfo
{
FileName = commandLine,
Arguments = arguments,
UseShellExecute = true
});
}
}
}
================================================
FILE: ClientCore/ProfanityFilter.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace ClientCore
{
public class ProfanityFilter
{
public IList CensoredWords { get; private set; }
///
/// Creates a new profanity filter with a default set of censored words.
///
public ProfanityFilter()
{
CensoredWords = new List()
{
"cunt*",
"*nigg*",
"paki*",
"shit",
"fuck*",
"admin*",
"allahu*",
"akbar",
"twat",
"cock",
"pussy",
"hitler*",
"anal",
"dick",
"faggot",
"whore",
"slut",
"motherfucker",
"asshole",
"bitch",
"bastard",
"kike",
"chink",
"spic",
"retard",
"tranny",
"jizz",
"gangbang",
"handjob",
"blowjob",
"rimjob",
"porn",
"rape",
"rapist",
"molest",
"incest",
"bestiality",
"zoophile",
"zoophilia",
"chingchong",
"slanty",
"zipperhead",
"gook",
};
}
public ProfanityFilter(IEnumerable censoredWords)
{
if (censoredWords == null)
throw new ArgumentNullException(nameof(censoredWords));
CensoredWords = new List(censoredWords);
}
public bool IsOffensive(string text)
{
foreach (string censoredWord in CensoredWords)
{
string regularExpression = ToRegexPattern(censoredWord);
if (Regex.IsMatch(text, regularExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
return true;
}
return false;
}
public string CensorText(string text)
{
if (text == null)
throw new ArgumentNullException(nameof(text));
string censoredText = text;
foreach (string censoredWord in CensoredWords)
{
string regularExpression = ToRegexPattern(censoredWord);
censoredText = Regex.Replace(censoredText, regularExpression, StarCensoredMatch,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
return censoredText;
}
private static string StarCensoredMatch(Match m)
{
string word = m.Captures[0].Value;
return new string('*', word.Length);
}
private string ToRegexPattern(string wildcardSearch)
{
string regexPattern = Regex.Escape(wildcardSearch);
regexPattern = regexPattern.Replace(@"\*", ".*?");
regexPattern = regexPattern.Replace(@"\?", ".");
if (regexPattern.StartsWith(".*?"))
{
regexPattern = regexPattern.Substring(3);
regexPattern = @"(^\b)*?" + regexPattern;
}
regexPattern = @"\b" + regexPattern + @"\b";
return regexPattern;
}
}
}
================================================
FILE: ClientCore/ProgramConstants.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Reflection;
using Rampastring.Tools;
using ClientCore.Extensions;
namespace ClientCore
{
///
/// Contains various static variables and constants that the client uses for operation.
///
public static class ProgramConstants
{
public static readonly string StartupExecutable = Assembly.GetEntryAssembly().Location;
public static readonly string StartupPath = SafePath.CombineDirectoryPath(new FileInfo(StartupExecutable).Directory.FullName);
public static readonly string GamePath = SafePath.CombineDirectoryPath(GetGamePath(StartupPath));
public static string ClientUserFilesPath => SafePath.CombineDirectoryPath(GamePath, "Client");
public static event EventHandler PlayerNameChanged;
public const string QRES_EXECUTABLE = "qres.dat";
public const string CNCNET_PROTOCOL_REVISION = "R14";
public const string LAN_PROTOCOL_REVISION = "RL8";
public const int LAN_PORT = 1234;
public const int LAN_INGAME_PORT = 1234;
public const int LAN_LOBBY_PORT = 1232;
public const int LAN_GAME_LOBBY_PORT = 1233;
public const char LAN_DATA_SEPARATOR = (char)01;
public const char LAN_MESSAGE_SEPARATOR = (char)02;
public const string SPAWNMAP_INI = "spawnmap.ini";
public const string SPAWNER_SETTINGS = "spawn.ini";
public const string SAVED_GAME_SPAWN_INI = "Saved Games/spawnSG.ini";
///
/// The locale code that corresponds to the language the hardcoded client strings are in.
///
public const string HARDCODED_LOCALE_CODE = "en";
///
/// Used to denote in the INI files.
///
///
/// Historically Westwood used '@' for this purpose, so we keep it for compatibility.
///
public const string INI_NEWLINE_PATTERN = "@";
public const int GAME_ID_MAX_LENGTH = 4;
public static readonly Encoding LAN_ENCODING = Encoding.UTF8;
#if NETFRAMEWORK
private static bool? isMono;
///
/// Gets a value whether or not the application is running under Mono. Uses lazy loading and caching.
///
public static bool ISMONO => isMono ??= Type.GetType("Mono.Runtime") != null;
#endif
public static string GAME_VERSION = "Undefined";
private static string PlayerName = "No name";
public static string PLAYERNAME
{
get { return PlayerName; }
set
{
string oldPlayerName = PlayerName;
PlayerName = value;
if (oldPlayerName != PlayerName)
PlayerNameChanged?.Invoke(null, EventArgs.Empty);
}
}
public static string BASE_RESOURCE_PATH = "Resources";
public static string RESOURCES_DIR = BASE_RESOURCE_PATH;
public static int LOG_LEVEL = 1;
public static bool IsInGame { get; set; }
public static string GetResourcePath()
{
return SafePath.CombineDirectoryPath(GamePath, RESOURCES_DIR);
}
public static string GetBaseResourcePath()
{
return SafePath.CombineDirectoryPath(GamePath, BASE_RESOURCE_PATH);
}
public const string GAME_INVITE_CTCP_COMMAND = "INVITE";
public const string GAME_INVITATION_FAILED_CTCP_COMMAND = "INVITATION_FAILED";
public static string GetAILevelName(int aiLevel)
{
if (aiLevel > -1 && aiLevel < AI_PLAYER_NAMES.Count)
return AI_PLAYER_NAMES[aiLevel];
return "";
}
public static readonly List TEAMS = new List { "A", "B", "C", "D" };
// Static fields might be initialized before the translation file is loaded. Change to readonly properties here.
public static List AI_PLAYER_NAMES => new List { "Easy AI".L10N("Client:Main:EasyAIName"), "Medium AI".L10N("Client:Main:MediumAIName"), "Hard AI".L10N("Client:Main:HardAIName") };
public static string LogFileName { get; set; }
///
/// This method finds the "Resources" directory by traversing the directory tree upwards from the startup path.
///
///
/// This method is needed by both ClientCore and DXMainClient. However, since it is usually called at the very beginning,
/// where DXMainClient could not refer to ClientCore, this method is copied to both projects.
/// Remember to keep and consistent if you have modified its source codes.
///
private static string SearchResourcesDir(string startupPath)
{
DirectoryInfo currentDir = new(startupPath);
for (int i = 0; i < 3; i++)
{
// Determine if currentDir is the "Resources" folder
if (currentDir.Name.ToLowerInvariant() == "Resources".ToLowerInvariant())
return currentDir.FullName;
// Additional check. This makes developers to debug the client inside Visual Studio a little bit easier.
DirectoryInfo resourcesDir = currentDir.GetDirectories("Resources", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (resourcesDir is not null)
return resourcesDir.FullName;
currentDir = currentDir.Parent;
}
throw new Exception("Could not find Resources directory.");
}
private static string GetGamePath(string startupPath)
{
string resourceDir = SearchResourcesDir(startupPath);
return new DirectoryInfo(resourceDir).Parent.FullName;
}
}
}
================================================
FILE: ClientCore/SavedGameManager.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using Rampastring.Tools;
namespace ClientCore
{
///
/// A class for handling saved multiplayer games.
///
public static class SavedGameManager
{
private const string SAVED_GAMES_DIRECTORY = "Saved Games";
private static bool saveRenameInProgress = false;
public static int GetSaveGameCount()
{
string saveGameDirectory = GetSaveGameDirectoryPath();
if (!AreSavedGamesAvailable())
return 0;
for (int i = 0; i < 1000; i++)
{
if (!SafePath.GetFile(saveGameDirectory, string.Format("SVGM_{0}.NET", i.ToString("D3"))).Exists)
{
return i;
}
}
return 1000;
}
public static List GetSaveGameTimestamps()
{
int saveGameCount = GetSaveGameCount();
List timestamps = new List();
string saveGameDirectory = GetSaveGameDirectoryPath();
for (int i = 0; i < saveGameCount; i++)
{
FileInfo sgFile = SafePath.GetFile(saveGameDirectory, string.Format("SVGM_{0}.NET", i.ToString("D3")));
DateTime dt = sgFile.LastWriteTime;
timestamps.Add(dt.ToString());
}
return timestamps;
}
public static bool AreSavedGamesAvailable()
{
if (Directory.Exists(GetSaveGameDirectoryPath()))
return true;
return false;
}
private static string GetSaveGameDirectoryPath()
{
return SafePath.CombineDirectoryPath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY);
}
///
/// Initializes saved MP games for a match.
///
public static bool InitSavedGames()
{
bool success = EraseSavedGames();
if (!success)
return false;
try
{
Logger.Log("Writing spawn.ini for saved game.");
SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini");
File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini"), SafePath.CombineFilePath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini"));
}
catch (Exception ex)
{
Logger.Log("Writing spawn.ini for saved game failed! Exception message: " + ex.ToString());
return false;
}
return true;
}
public static void RenameSavedGame()
{
Logger.Log("Renaming saved game.");
if (saveRenameInProgress)
{
Logger.Log("Save renaming in progress!");
return;
}
string saveGameDirectory = GetSaveGameDirectoryPath();
if (!SafePath.GetFile(saveGameDirectory, "SAVEGAME.NET").Exists)
{
Logger.Log("SAVEGAME.NET doesn't exist!");
return;
}
saveRenameInProgress = true;
int saveGameId = 0;
for (int i = 0; i < 1000; i++)
{
if (!SafePath.GetFile(saveGameDirectory, string.Format("SVGM_{0}.NET", i.ToString("D3"))).Exists)
{
saveGameId = i;
break;
}
}
if (saveGameId == 999)
{
if (SafePath.GetFile(saveGameDirectory, "SVGM_999.NET").Exists)
Logger.Log("1000 saved games exceeded! Overwriting previous MP save.");
}
string sgPath = SafePath.CombineFilePath(saveGameDirectory, string.Format("SVGM_{0}.NET", saveGameId.ToString("D3")));
int tryCount = 0;
while (true)
{
try
{
File.Move(SafePath.CombineFilePath(saveGameDirectory, "SAVEGAME.NET"), sgPath);
break;
}
catch (Exception ex)
{
Logger.Log("Renaming saved game failed! Exception message: " + ex.ToString());
}
tryCount++;
if (tryCount > 40)
{
Logger.Log("Renaming saved game failed 40 times! Aborting.");
return;
}
System.Threading.Thread.Sleep(250);
}
saveRenameInProgress = false;
Logger.Log("Saved game SAVEGAME.NET succesfully renamed to " + Path.GetFileName(sgPath));
}
public static bool EraseSavedGames()
{
Logger.Log("Erasing previous MP saved games.");
try
{
for (int i = 0; i < 1000; i++)
{
SafePath.DeleteFileIfExists(GetSaveGameDirectoryPath(), string.Format("SVGM_{0}.NET", i.ToString("D3")));
}
}
catch (Exception ex)
{
Logger.Log("Erasing previous MP saved games failed! Exception message: " + ex.ToString());
return false;
}
Logger.Log("MP saved games succesfully erased.");
return true;
}
}
}
================================================
FILE: ClientCore/Settings/BoolSetting.cs
================================================
using Rampastring.Tools;
namespace ClientCore.Settings
{
public class BoolSetting : INISetting
{
public BoolSetting(IniFile iniFile, string iniSection, string iniKey, bool defaultValue)
: base(iniFile, iniSection, iniKey, defaultValue)
{
}
protected override bool Get()
{
return IniFile.GetBooleanValue(IniSection, IniKey, DefaultValue);
}
protected override void Set(bool value)
{
IniFile.SetBooleanValue(IniSection, IniKey, value);
}
public override void Write()
{
IniFile.SetBooleanValue(IniSection, IniKey, Get());
}
public override string ToString()
{
return Get().ToString();
}
}
}
================================================
FILE: ClientCore/Settings/DoubleSetting.cs
================================================
using Rampastring.Tools;
namespace ClientCore.Settings
{
public class DoubleSetting : INISetting
{
public DoubleSetting(IniFile iniFile, string iniSection, string iniKey, double defaultValue)
: base(iniFile, iniSection, iniKey, defaultValue)
{
}
protected override double Get()
{
return IniFile.GetDoubleValue(IniSection, IniKey, DefaultValue);
}
protected override void Set(double value)
{
IniFile.SetDoubleValue(IniSection, IniKey, value);
}
public override void Write()
{
IniFile.SetDoubleValue(IniSection, IniKey, Get());
}
public override string ToString()
{
return Get().ToString();
}
}
}
================================================
FILE: ClientCore/Settings/IIniSetting.cs
================================================
namespace ClientCore.Settings
{
///
/// A dummy interface for checking for INISetting in reflection.
///
interface IIniSetting
{
}
}
================================================
FILE: ClientCore/Settings/INISetting.cs
================================================
using Rampastring.Tools;
namespace ClientCore.Settings
{
///
/// A base class for an INI setting.
///
public abstract class INISetting : IIniSetting
{
public INISetting(IniFile iniFile, string iniSection, string iniKey,
T defaultValue)
{
IniFile = iniFile;
IniSection = iniSection;
IniKey = iniKey;
DefaultValue = defaultValue;
}
public static implicit operator T(INISetting iniSetting)
{
return iniSetting.Get();
}
public void SetIniFile(IniFile iniFile)
{
IniFile = iniFile;
}
protected IniFile IniFile { get; private set; }
protected string IniSection { get; private set; }
protected string IniKey { get; private set; }
protected T DefaultValue { get; private set; }
public T Value
{
get { return Get(); }
set { Set(value); }
}
///
/// Writes the default value of this setting to the INI file if no value
/// for the setting is currently specified in the INI file.
///
public void SetDefaultIfNonexistent()
{
if (!IniFile.KeyExists(IniSection, IniKey))
Set(DefaultValue);
}
protected abstract T Get();
protected abstract void Set(T value);
public abstract void Write();
}
}
================================================
FILE: ClientCore/Settings/IntRangeSetting.cs
================================================
using Rampastring.Tools;
namespace ClientCore.Settings
{
///
/// Similar to IntSetting, this setting forces a min and max value upon getting and setting.
///
public class IntRangeSetting : IntSetting
{
private readonly int MinValue;
private readonly int MaxValue;
public IntRangeSetting(IniFile iniFile, string iniSection, string iniKey, int defaultValue, int minValue, int maxValue) : base(iniFile, iniSection, iniKey, defaultValue)
{
MinValue = minValue;
MaxValue = maxValue;
}
///
/// Checks the validity of the value. If the value is invalid, return the default value of this setting.
/// Otherwise, return the set value.
///
///
///
private int NormalizeValue(int value)
{
return InvalidValue(value) ? DefaultValue : value;
}
private bool InvalidValue(int value)
{
return value < MinValue || value > MaxValue;
}
protected override int Get()
{
return NormalizeValue(IniFile.GetIntValue(IniSection, IniKey, DefaultValue));
}
protected override void Set(int value)
{
IniFile.SetIntValue(IniSection, IniKey, NormalizeValue(value));
}
}
}
================================================
FILE: ClientCore/Settings/IntSetting.cs
================================================
using Rampastring.Tools;
namespace ClientCore.Settings
{
public class IntSetting : INISetting
{
public IntSetting(IniFile iniFile, string iniSection, string iniKey, int defaultValue)
: base(iniFile, iniSection, iniKey, defaultValue)
{
}
protected override int Get()
{
return IniFile.GetIntValue(IniSection, IniKey, DefaultValue);
}
protected override void Set(int value)
{
IniFile.SetIntValue(IniSection, IniKey, value);
}
public override void Write()
{
IniFile.SetIntValue(IniSection, IniKey, Get());
}
public override string ToString()
{
return Get().ToString();
}
}
}
================================================
FILE: ClientCore/Settings/StringListSetting.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Rampastring.Tools;
namespace ClientCore.Settings
{
///
/// This is a setting that can be stored as a comma separated list of strings.
///
public class StringListSetting : INISetting>
{
public StringListSetting(IniFile iniFile, string iniSection, string iniKey, List defaultValue) : base(iniFile, iniSection, iniKey, defaultValue)
{
}
protected override List Get()
{
string value = IniFile.GetStringValue(IniSection, IniKey, "");
return string.IsNullOrWhiteSpace(value) ? DefaultValue : value.Split(',').ToList();
}
protected override void Set(List value)
{
IniFile.SetStringValue(IniSection, IniKey, string.Join(",", value));
}
public override void Write()
{
IniFile.SetStringValue(IniSection, IniKey, string.Join(",", Get()));
}
public void Add(string value)
{
var values = Get().Concat(new []{value}).ToList();
Set(values);
}
public void Remove(string value)
{
var values = Get().Where(v => !string.Equals(v, value, StringComparison.InvariantCultureIgnoreCase)).ToList();
Set(values);
}
}
}
================================================
FILE: ClientCore/Settings/StringSetting.cs
================================================
using Rampastring.Tools;
namespace ClientCore.Settings
{
public class StringSetting : INISetting
{
public StringSetting(IniFile iniFile, string iniSection, string iniKey, string defaultValue)
: base(iniFile, iniSection, iniKey, defaultValue)
{
}
protected override string Get()
{
return IniFile.GetStringValue(IniSection, IniKey, DefaultValue);
}
protected override void Set(string value)
{
IniFile.SetStringValue(IniSection, IniKey, value);
}
public override void Write()
{
IniFile.SetStringValue(IniSection, IniKey, Get());
}
public override string ToString()
{
return Get();
}
}
}
================================================
FILE: ClientCore/Settings/UserINISettings.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ClientCore.Enums;
using ClientCore.Extensions;
using ClientCore.Settings;
using Rampastring.Tools;
namespace ClientCore
{
public class UserINISettings
{
private static UserINISettings _instance;
public const string VIDEO = "Video";
public const string MULTIPLAYER = "MultiPlayer";
public const string OPTIONS = "Options";
public const string AUDIO = "Audio";
public const string COMPATIBILITY = "Compatibility";
public const string GAME_FILTERS = "GameFilters";
public const string GAME_OPTION_FILTERS = "GameOptionFilters";
private const string FAVORITE_MAPS = "FavoriteMaps";
private const bool DEFAULT_SHOW_FRIENDS_ONLY_GAMES = false;
private const bool DEFAULT_HIDE_LOCKED_GAMES = false;
private const bool DEFAULT_HIDE_PASSWORDED_GAMES = false;
private const bool DEFAULT_HIDE_INCOMPATIBLE_GAMES = false;
private const int DEFAULT_MAX_PLAYER_COUNT = 8;
public static UserINISettings Instance
{
get
{
if (_instance == null)
throw new InvalidOperationException("UserINISettings not initialized!");
return _instance;
}
}
public static void Initialize(string userIniFileName)
{
if (_instance != null)
throw new InvalidOperationException("UserINISettings has already been initialized!");
var userIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, userIniFileName));
string userDefaultIniFilePath = SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), "UserDefaults.ini");
if (!File.Exists(userDefaultIniFilePath))
{
_instance = new UserINISettings(userIni);
return;
}
var userDefaultIni = new IniFile(userDefaultIniFilePath);
var combinedUserIni = userDefaultIni.Clone();
combinedUserIni.FileName = null;
// Combine userIni and userDefaultIni
foreach (string sectionName in userIni.GetSections())
{
IniSection userSection = userIni.GetSection(sectionName);
IniSection combinedUserSection = combinedUserIni.GetSection(sectionName);
if (combinedUserSection == null)
{
combinedUserSection = new IniSection(sectionName);
combinedUserIni.AddSection(combinedUserSection);
}
foreach ((var key, var value) in userSection.Keys)
{
combinedUserSection.AddOrReplaceKey(key, value);
}
}
combinedUserIni.FileName = userIni.FileName;
_instance = new UserINISettings(combinedUserIni);
}
protected UserINISettings(IniFile iniFile)
{
SettingsIni = iniFile;
if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)
BackBufferInVRAM = new BoolSetting(iniFile, VIDEO, "UseGraphicsPatch", true);
else
BackBufferInVRAM = new BoolSetting(iniFile, VIDEO, "VideoBackBuffer", false);
IngameScreenWidth = new IntSetting(
iniFile,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : VIDEO,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? "Width" : "ScreenWidth",
1024);
IngameScreenHeight = new IntSetting(
iniFile,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : VIDEO,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? "Height" : "ScreenHeight",
768);
ClientTheme = new StringSetting(iniFile, MULTIPLAYER, "Theme", ClientConfiguration.Instance.GetThemeInfoFromIndex(0).Name);
Translation = new StringSetting(iniFile, OPTIONS, "Translation", I18N.Translation.GetDefaultTranslationLocaleCode());
TranslationGameFilesVersion = new StringSetting(iniFile, OPTIONS, nameof(TranslationGameFilesVersion), string.Empty);
DetailLevel = new IntSetting(iniFile, OPTIONS, "DetailLevel", 2);
Renderer = new StringSetting(iniFile, COMPATIBILITY, "Renderer", string.Empty);
WindowedMode = new BoolSetting(iniFile, VIDEO, ClientConfiguration.Instance.WindowedModeKey, false);
BorderlessWindowedMode = new BoolSetting(iniFile, VIDEO, "NoWindowFrame", false);
BorderlessWindowedClient = new BoolSetting(iniFile, VIDEO, "BorderlessWindowedClient", ClientConfiguration.Instance.UserDefault_BorderlessWindowedClient);
IntegerScaledClient = new BoolSetting(iniFile, VIDEO, "IntegerScaledClient", ClientConfiguration.Instance.UserDefault_IntegerScaledClient);
ClientFPS = new IntSetting(iniFile, VIDEO, "ClientFPS", 60);
DisplayToggleableExtraTextures = new BoolSetting(iniFile, VIDEO, "DisplayToggleableExtraTextures", true);
// RA1 reads MultiplayerScoreVolume instead of ScoreVolume. This value is handled when saving
ScoreVolume = new DoubleSetting(iniFile,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : AUDIO,
"ScoreVolume",
0.7);
SoundVolume = new DoubleSetting(iniFile,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? OPTIONS : AUDIO,
ClientConfiguration.Instance.ClientGameType == ClientType.RA ? "Volume" : "SoundVolume",
0.7);
VoiceVolume = new DoubleSetting(iniFile, AUDIO, "VoiceVolume", 0.7);
IsScoreShuffle = new BoolSetting(iniFile, AUDIO, "IsScoreShuffle", true);
ClientVolume = new DoubleSetting(iniFile, AUDIO, "ClientVolume", 1.0);
PlayMainMenuMusic = new BoolSetting(iniFile, AUDIO, "PlayMainMenuMusic", true);
StopMusicOnMenu = new BoolSetting(iniFile, AUDIO, "StopMusicOnMenu", true);
StopGameLobbyMessageAudio = new BoolSetting(iniFile, AUDIO, "StopGameLobbyMessageAudio", true);
MessageSound = new BoolSetting(iniFile, AUDIO, "ChatMessageSound", true);
ScrollRate = new IntSetting(iniFile, OPTIONS, "ScrollRate", 3);
DragDistance = new IntSetting(iniFile, OPTIONS, "DragDistance", 4);
CustomDragDistance = new IntSetting(iniFile, OPTIONS, "CustomDragDistance", 0);
DoubleTapInterval = new IntSetting(iniFile, OPTIONS, "DoubleTapInterval", 30);
Win8CompatMode = new StringSetting(iniFile, OPTIONS, "Win8Compat", "No");
PlayerName = new StringSetting(iniFile, MULTIPLAYER, "Handle", string.Empty);
ChatColor = new IntSetting(iniFile, MULTIPLAYER, "ChatColor", -1);
LANChatColor = new IntSetting(iniFile, MULTIPLAYER, "LANChatColor", -1);
PingUnofficialCnCNetTunnels = new BoolSetting(iniFile, MULTIPLAYER, "PingCustomTunnels", true);
WritePathToRegistry = new BoolSetting(iniFile, OPTIONS, "WriteInstallationPathToRegistry", ClientConfiguration.Instance.UserDefault_WriteInstallationPathToRegistry);
PlaySoundOnGameHosted = new BoolSetting(iniFile, MULTIPLAYER, "PlaySoundOnGameHosted", true);
SkipConnectDialog = new BoolSetting(iniFile, MULTIPLAYER, "SkipConnectDialog", false);
PersistentMode = new BoolSetting(iniFile, MULTIPLAYER, "PersistentMode", false);
AutomaticCnCNetLogin = new BoolSetting(iniFile, MULTIPLAYER, "AutomaticCnCNetLogin", false);
DiscordIntegration = new BoolSetting(iniFile, MULTIPLAYER, "DiscordIntegration", true);
SteamIntegration = new BoolSetting(iniFile, MULTIPLAYER, "SteamIntegration", true);
AllowGameInvitesFromFriendsOnly = new BoolSetting(iniFile, MULTIPLAYER, "AllowGameInvitesFromFriendsOnly", false);
NotifyOnUserListChange = new BoolSetting(iniFile, MULTIPLAYER, "NotifyOnUserListChange", true);
DisablePrivateMessagePopups = new BoolSetting(iniFile, MULTIPLAYER, "DisablePrivateMessagePopups", false);
AllowPrivateMessagesFromState = new IntSetting(iniFile, MULTIPLAYER, "AllowPrivateMessagesFromState", (int)AllowPrivateMessagesFromEnum.All);
EnableMapSharing = new BoolSetting(iniFile, MULTIPLAYER, "EnableMapSharing", true);
AlwaysDisplayTunnelList = new BoolSetting(iniFile, MULTIPLAYER, "AlwaysDisplayTunnelList", false);
MapSortState = new IntSetting(iniFile, MULTIPLAYER, "MapSortState", (int)SortDirection.None);
SearchAllGameModes = new BoolSetting(iniFile, MULTIPLAYER, "SearchAllGameModes", false);
CheckForUpdates = new BoolSetting(iniFile, OPTIONS, "CheckforUpdates", true);
PrivacyPolicyAccepted = new BoolSetting(iniFile, OPTIONS, "PrivacyPolicyAccepted", false);
IsFirstRun = new BoolSetting(iniFile, OPTIONS, "IsFirstRun", true);
CustomComponentsDenied = new BoolSetting(iniFile, OPTIONS, "CustomComponentsDenied", false);
Difficulty = new IntSetting(iniFile, OPTIONS, "Difficulty", 1);
ScrollDelay = new IntSetting(iniFile, OPTIONS, "ScrollDelay", 4);
GameSpeed = new IntSetting(iniFile, OPTIONS, "GameSpeed", 1);
ForceLowestDetailLevel = new BoolSetting(iniFile, VIDEO, "ForceLowestDetailLevel", false);
MinimizeWindowsOnGameStart = new BoolSetting(iniFile, OPTIONS, "MinimizeWindowsOnGameStart", true);
AutoRemoveUnderscoresFromName = new BoolSetting(iniFile, OPTIONS, "AutoRemoveUnderscoresFromName", true);
GenerateTranslationStub = new BoolSetting(iniFile, OPTIONS, nameof(GenerateTranslationStub), false);
GenerateOnlyNewValuesInTranslationStub = new BoolSetting(iniFile, OPTIONS, nameof(GenerateOnlyNewValuesInTranslationStub), false);
SortState = new IntSetting(iniFile, GAME_FILTERS, "SortState", (int)SortDirection.None);
ShowFriendGamesOnly = new BoolSetting(iniFile, GAME_FILTERS, "ShowFriendGamesOnly", DEFAULT_SHOW_FRIENDS_ONLY_GAMES);
HideLockedGames = new BoolSetting(iniFile, GAME_FILTERS, "HideLockedGames", DEFAULT_HIDE_LOCKED_GAMES);
HidePasswordedGames = new BoolSetting(iniFile, GAME_FILTERS, "HidePasswordedGames", DEFAULT_HIDE_PASSWORDED_GAMES);
HideIncompatibleGames = new BoolSetting(iniFile, GAME_FILTERS, "HideIncompatibleGames", DEFAULT_HIDE_INCOMPATIBLE_GAMES);
MaxPlayerCount = new IntRangeSetting(iniFile, GAME_FILTERS, "MaxPlayerCount", DEFAULT_MAX_PLAYER_COUNT, 2, 8);
LoadFavoriteMaps(iniFile);
}
public IniFile SettingsIni { get; private set; }
public event EventHandler SettingsSaved;
/*********/
/* VIDEO */
/*********/
public IntSetting IngameScreenWidth { get; private set; }
public IntSetting IngameScreenHeight { get; private set; }
public StringSetting ClientTheme { get; private set; }
public string ThemeFolderPath => ClientConfiguration.Instance.GetThemePath(ClientTheme);
public StringSetting Translation { get; private set; }
public StringSetting TranslationGameFilesVersion { get; private set; }
public string TranslationFolderPath => SafePath.CombineDirectoryPath(
ClientConfiguration.Instance.TranslationsFolderPath, Translation);
public string TranslationThemeFolderPath => SafePath.CombineDirectoryPath(
ClientConfiguration.Instance.TranslationsFolderPath, Translation,
ClientConfiguration.Instance.GetThemePath(ClientTheme));
public IntSetting DetailLevel { get; private set; }
public StringSetting Renderer { get; private set; }
public BoolSetting WindowedMode { get; private set; }
public BoolSetting BorderlessWindowedMode { get; private set; }
public BoolSetting BackBufferInVRAM { get; private set; }
public IntSetting ClientResolutionX { get; set; }
public IntSetting ClientResolutionY { get; set; }
public BoolSetting BorderlessWindowedClient { get; private set; }
public BoolSetting IntegerScaledClient { get; private set; }
public IntSetting ClientFPS { get; private set; }
public BoolSetting DisplayToggleableExtraTextures { get; private set; }
/*********/
/* AUDIO */
/*********/
public DoubleSetting ScoreVolume { get; private set; }
public DoubleSetting SoundVolume { get; private set; }
public DoubleSetting VoiceVolume { get; private set; }
public BoolSetting IsScoreShuffle { get; private set; }
public DoubleSetting ClientVolume { get; private set; }
public BoolSetting PlayMainMenuMusic { get; private set; }
public BoolSetting StopMusicOnMenu { get; private set; }
public BoolSetting StopGameLobbyMessageAudio { get; private set; }
public BoolSetting MessageSound { get; private set; }
/********/
/* GAME */
/********/
public IntSetting ScrollRate { get; private set; }
public IntSetting DragDistance { get; private set; }
// When > 0, overrides the auto-scaled DragDistance. Allows players to set a fixed pixel threshold regardless of resolution.
public IntSetting CustomDragDistance { get; private set; }
public IntSetting DoubleTapInterval { get; private set; }
public StringSetting Win8CompatMode { get; private set; }
/************************/
/* MULTIPLAYER (CnCNet) */
/************************/
public StringSetting PlayerName { get; private set; }
public IntSetting ChatColor { get; private set; }
public IntSetting LANChatColor { get; private set; }
public BoolSetting PingUnofficialCnCNetTunnels { get; private set; }
public BoolSetting WritePathToRegistry { get; private set; }
public BoolSetting PlaySoundOnGameHosted { get; private set; }
public BoolSetting SkipConnectDialog { get; private set; }
public BoolSetting PersistentMode { get; private set; }
public BoolSetting AutomaticCnCNetLogin { get; private set; }
public BoolSetting DiscordIntegration { get; private set; }
public BoolSetting SteamIntegration { get; private set; }
public BoolSetting AllowGameInvitesFromFriendsOnly { get; private set; }
public BoolSetting NotifyOnUserListChange { get; private set; }
public BoolSetting DisablePrivateMessagePopups { get; private set; }
public IntSetting AllowPrivateMessagesFromState { get; private set; }
public BoolSetting EnableMapSharing { get; private set; }
public BoolSetting AlwaysDisplayTunnelList { get; private set; }
public IntSetting MapSortState { get; private set; }
public BoolSetting SearchAllGameModes { get; private set; }
/*********************/
/* GAME LIST FILTERS */
/*********************/
public IntSetting SortState { get; private set; }
public BoolSetting ShowFriendGamesOnly { get; private set; }
public BoolSetting HideLockedGames { get; private set; }
public BoolSetting HidePasswordedGames { get; private set; }
public BoolSetting HideIncompatibleGames { get; private set; }
public IntRangeSetting MaxPlayerCount { get; private set; }
/************************/
/* GAME OPTION FILTERS */
/************************/
///
/// Gets the filter value for a game option (checkbox or dropdown).
/// Returns null for "All" (no filter), or the selected index.
/// For checkboxes: 0 = Off, 1 = On.
/// For dropdowns: 0+ = actual option index.
///
public int? GetGameOptionFilterValue(string optionName)
{
var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);
if (section == null || !section.KeyExists(optionName))
return null;
return section.GetIntValue(optionName, 0);
}
///
/// Sets the filter value for a game option.
/// null = "All" (no filter), or the selected index.
/// When null, removes the key from INI. Otherwise stores the index value.
/// For checkboxes: 0 = Off, 1 = On.
/// For dropdowns: 0+ = actual option index.
///
public void SetGameOptionFilterValue(string optionName, int? value)
{
if (value == null)
SettingsIni.GetSection(GAME_OPTION_FILTERS)?.RemoveKey(optionName);
else
SettingsIni.SetIntValue(GAME_OPTION_FILTERS, optionName, value.Value);
}
/********/
/* MISC */
/********/
public BoolSetting CheckForUpdates { get; private set; }
public BoolSetting PrivacyPolicyAccepted { get; private set; }
public BoolSetting IsFirstRun { get; private set; }
public BoolSetting CustomComponentsDenied { get; private set; }
public IntSetting Difficulty { get; private set; }
public IntSetting GameSpeed { get; private set; }
public IntSetting ScrollDelay { get; private set; }
public BoolSetting ForceLowestDetailLevel { get; private set; }
public BoolSetting MinimizeWindowsOnGameStart { get; private set; }
public BoolSetting AutoRemoveUnderscoresFromName { get; private set; }
public BoolSetting GenerateTranslationStub { get; private set; }
public BoolSetting GenerateOnlyNewValuesInTranslationStub { get; private set; }
public List FavoriteMaps { get; private set; }
public void SetValue(string section, string key, string value)
=> SettingsIni.SetStringValue(section, key, value);
public void SetValue(string section, string key, bool value)
=> SettingsIni.SetBooleanValue(section, key, value);
public void SetValue(string section, string key, int value)
=> SettingsIni.SetIntValue(section, key, value);
public string GetValue(string section, string key, string defaultValue)
=> SettingsIni.GetStringValue(section, key, defaultValue);
public bool GetValue(string section, string key, bool defaultValue)
=> SettingsIni.GetBooleanValue(section, key, defaultValue);
public int GetValue(string section, string key, int defaultValue)
=> SettingsIni.GetIntValue(section, key, defaultValue);
public bool IsGameFollowed(string gameName)
=> SettingsIni.GetBooleanValue("Channels", gameName, false);
public bool ToggleFavoriteMap(string mapSHA1, string gameModeName, bool isFavorite)
{
if (string.IsNullOrEmpty(mapSHA1))
return isFavorite;
string favoriteMapKey = FavoriteMapKey(mapSHA1, gameModeName);
bool isCurrentlyFavorite = FavoriteMaps.Contains(favoriteMapKey);
if (isCurrentlyFavorite)
FavoriteMaps.Remove(favoriteMapKey);
else
FavoriteMaps.Add(favoriteMapKey);
Instance.SaveSettings();
WriteFavoriteMaps();
return !isCurrentlyFavorite;
}
private void LoadFavoriteMaps(IniFile iniFile)
{
FavoriteMaps = new List();
bool legacyMapsLoaded = LoadLegacyFavoriteMaps(iniFile);
var favoriteMapsSection = SettingsIni.GetOrAddSection(FAVORITE_MAPS);
foreach (KeyValuePair keyValuePair in favoriteMapsSection.Keys)
FavoriteMaps.Add(keyValuePair.Value);
if (legacyMapsLoaded)
WriteFavoriteMaps();
}
public void WriteFavoriteMaps()
{
var favoriteMapsSection = SettingsIni.GetOrAddSection(FAVORITE_MAPS);
favoriteMapsSection.RemoveAllKeys();
for (int i = 0; i < FavoriteMaps.Count; i++)
favoriteMapsSection.AddKey(i.ToString(), FavoriteMaps[i]);
SaveSettings();
}
///
/// Checks if a specified map name and game mode name belongs to the favorite map list.
/// Name-based favorites are migrated to SHA1.
///
/// The SHA1 hash of the map.
/// The name of the map.
/// The name of the game mode.
public bool IsFavoriteMap(string mapSHA1, string mapName, string gameModeName)
{
// SHA1-based lookup first
if (!string.IsNullOrEmpty(mapSHA1) && FavoriteMaps.Contains(FavoriteMapKey(mapSHA1, gameModeName)))
return true;
// Fallback to name-based
string nameKey = FavoriteMapKey(mapName, gameModeName);
if (FavoriteMaps.Contains(nameKey))
{
// Migrate to SHA1
if (!string.IsNullOrEmpty(mapSHA1))
{
string sha1Key = FavoriteMapKey(mapSHA1, gameModeName);
if (!FavoriteMaps.Contains(sha1Key))
{
FavoriteMaps.Add(sha1Key);
WriteFavoriteMaps();
}
// Note: We don't remove the name-based entry here to allow other maps
// with the same name to also migrate. The name-based entry will be
// cleaned up when all maps with that name have been processed.
}
return true;
}
return false;
}
private string FavoriteMapKey(string identifier, string gameModeName) => $"{identifier}:{gameModeName}";
public void ReloadSettings() => SettingsIni.Reload();
public void ApplyDefaults()
{
ForceLowestDetailLevel.SetDefaultIfNonexistent();
DoubleTapInterval.SetDefaultIfNonexistent();
ScrollDelay.SetDefaultIfNonexistent();
}
public void SaveSettings()
{
Logger.Log("Writing settings INI.");
ApplyDefaults();
// CleanUpLegacySettings();
// RA1 reads MultiplayerScoreVolume instead of ScoreVolume
if (ClientConfiguration.Instance.ClientGameType == ClientType.RA)
SettingsIni.SetDoubleValue(OPTIONS, "MultiplayerScoreVolume", SettingsIni.GetDoubleValue(OPTIONS, "ScoreVolume", 0.7));
SettingsIni.WriteIniFile();
SettingsSaved?.Invoke(this, EventArgs.Empty);
}
public bool IsGameFiltersApplied()
=> ShowFriendGamesOnly.Value != DEFAULT_SHOW_FRIENDS_ONLY_GAMES
|| HideLockedGames.Value != DEFAULT_HIDE_LOCKED_GAMES
|| HidePasswordedGames.Value != DEFAULT_HIDE_PASSWORDED_GAMES
|| HideIncompatibleGames.Value != DEFAULT_HIDE_INCOMPATIBLE_GAMES
|| MaxPlayerCount.Value != DEFAULT_MAX_PLAYER_COUNT
|| HasGameOptionFilters();
public void ResetGameFilters()
{
ShowFriendGamesOnly.Value = DEFAULT_SHOW_FRIENDS_ONLY_GAMES;
HideLockedGames.Value = DEFAULT_HIDE_LOCKED_GAMES;
HideIncompatibleGames.Value = DEFAULT_HIDE_INCOMPATIBLE_GAMES;
HidePasswordedGames.Value = DEFAULT_HIDE_PASSWORDED_GAMES;
MaxPlayerCount.Value = DEFAULT_MAX_PLAYER_COUNT;
ResetGameOptionFilters();
}
///
/// Checks if any game option filters are set.
///
private bool HasGameOptionFilters()
{
var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);
return section != null && section.Keys.Count > 0;
}
///
/// Clears all game option filters.
///
private void ResetGameOptionFilters()
{
var section = SettingsIni.GetSection(GAME_OPTION_FILTERS);
section?.RemoveAllKeys();
}
///
/// Used to remove old sections/keys to avoid confusion when viewing the ini file directly.
///
private void CleanUpLegacySettings()
=> SettingsIni.GetSection(GAME_FILTERS).RemoveKey("SortAlpha");
///
/// Previously, favorite maps were stored under a single key under the [Options] section.
/// This attempts to read in that legacy key.
///
///
/// Whether or not legacy favorites were loaded.
private bool LoadLegacyFavoriteMaps(IniFile iniFile)
{
var legacyFavoriteMaps = new StringListSetting(iniFile, OPTIONS, FAVORITE_MAPS, new List());
if (!legacyFavoriteMaps.Value?.Any() ?? true)
return false;
foreach (string favoriteMapKey in legacyFavoriteMaps.Value)
FavoriteMaps.Add(favoriteMapKey);
// remove the old key
iniFile.GetSection(OPTIONS).RemoveKey(FAVORITE_MAPS);
return true;
}
}
}
================================================
FILE: ClientCore/Statistics/DataWriter.cs
================================================
using System;
using System.Buffers.Binary;
using System.IO;
using System.Text;
namespace ClientCore.Statistics
{
internal static class DataWriter
{
public static void WriteInt(this Stream stream, int value)
{
byte[] buffer = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
stream.Write(buffer, 0, sizeof(int));
}
public static void WriteLong(this Stream stream, long value)
{
byte[] buffer = new byte[sizeof(long)];
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
stream.Write(buffer, 0, sizeof(long));
}
public static void WriteBool(this Stream stream, bool value)
{
stream.WriteByte(Convert.ToByte(value));
}
public static void WriteString(this Stream stream, string value, int reservedSpace, Encoding encoding = null)
{
if (encoding == null)
encoding = Encoding.Unicode;
byte[] writeBuffer = encoding.GetBytes(value);
if (writeBuffer.Length != reservedSpace)
{
// If the name's byte presentation is not equal to reservedSpace,
// let's resize the array
byte[] temp = writeBuffer;
writeBuffer = new byte[reservedSpace];
for (int j = 0; j < temp.Length && j < writeBuffer.Length; j++)
writeBuffer[j] = temp[j];
}
stream.Write(writeBuffer, 0, writeBuffer.Length);
}
}
}
================================================
FILE: ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using Rampastring.Tools;
namespace ClientCore.Statistics.GameParsers
{
public class LogFileStatisticsParser : GenericMatchParser
{
public LogFileStatisticsParser(MatchStatistics ms, bool isLoadedGame) : base(ms)
{
this.isLoadedGame = isLoadedGame;
}
private string fileName = "DTA.log";
private string economyString = "Economy"; // RA2/YR do not have economy stat, but a number of built objects.
private bool isLoadedGame;
public void ParseStats(string gamepath, string fileName)
{
this.fileName = fileName;
if (ClientConfiguration.Instance.UseBuiltStatistic) economyString = "Built";
ParseStatistics(gamepath);
}
protected override void ParseStatistics(string gamepath)
{
FileInfo statisticsFileInfo = SafePath.GetFile(gamepath, fileName);
if (!statisticsFileInfo.Exists)
{
Logger.Log("DTAStatisticsParser: Failed to read statistics: the log file does not exist.");
return;
}
Logger.Log("Attempting to read statistics from " + fileName);
try
{
using StreamReader reader = new StreamReader(statisticsFileInfo.OpenRead());
string line;
List takeoverAIs = new List();
PlayerStatistics currentPlayer = null;
bool sawCompletion = false;
int numPlayersFound = 0;
while ((line = reader.ReadLine()) != null)
{
if (line.Contains(": Loser"))
{
// Player found, game saw completion
sawCompletion = true;
string playerName = line.Substring(0, line.Length - 7);
currentPlayer = Statistics.GetEmptyPlayerByName(playerName);
if (isLoadedGame && currentPlayer == null)
currentPlayer = Statistics.Players.Find(p => p.Name == playerName);
Logger.Log("Found player " + playerName);
numPlayersFound++;
if (currentPlayer == null && playerName == "Computer" && numPlayersFound <= Statistics.NumberOfHumanPlayers)
{
// The player has been taken over by an AI during the match
Logger.Log("Losing take-over AI found");
takeoverAIs.Add(new PlayerStatistics("Computer", false, true, false, 0, 10, 255, 1));
currentPlayer = takeoverAIs[takeoverAIs.Count - 1];
}
if (currentPlayer != null)
currentPlayer.SawEnd = true;
}
else if (line.Contains(": Winner"))
{
// Player found, game saw completion
sawCompletion = true;
string playerName = line.Substring(0, line.Length - 8);
currentPlayer = Statistics.GetEmptyPlayerByName(playerName);
if (isLoadedGame && currentPlayer == null)
currentPlayer = Statistics.Players.Find(p => p.Name == playerName);
Logger.Log("Found player " + playerName);
numPlayersFound++;
if (currentPlayer == null && playerName == "Computer" && numPlayersFound <= Statistics.NumberOfHumanPlayers)
{
// The player has been taken over by an AI during the match
Logger.Log("Winning take-over AI found");
takeoverAIs.Add(new PlayerStatistics("Computer", false, true, false, 0, 10, 255, 1));
currentPlayer = takeoverAIs[takeoverAIs.Count - 1];
}
if (currentPlayer != null)
{
currentPlayer.SawEnd = true;
currentPlayer.Won = true;
}
}
else if (line.Contains("Game loop finished. Average FPS"))
{
// Game loop finished. Average FPS =
string fpsString = line.Substring(34);
Statistics.AverageFPS = Int32.Parse(fpsString);
}
if (currentPlayer == null || line.Length < 1)
continue;
line = line.Substring(1);
if (line.StartsWith("Lost = "))
currentPlayer.Losses = Int32.Parse(line.Substring(7));
else if (line.StartsWith("Kills = "))
currentPlayer.Kills = Int32.Parse(line.Substring(8));
else if (line.StartsWith("Score = "))
currentPlayer.Score = Int32.Parse(line.Substring(8));
else if (line.StartsWith(economyString + " = "))
currentPlayer.Economy = Int32.Parse(line.Substring(economyString.Length + 2));
}
// Check empty players for take-over by AIs
if (takeoverAIs.Count == 1)
{
PlayerStatistics ai = takeoverAIs[0];
PlayerStatistics ps = Statistics.GetFirstEmptyPlayer();
ps.Losses = ai.Losses;
ps.Kills = ai.Kills;
ps.Score = ai.Score;
ps.Economy = ai.Economy;
}
else if (takeoverAIs.Count > 1)
{
// If there's multiple take-over AI players, we have no way of figuring out
// which AI represents which player, so let's just add the AIs into the player list
// (then the user viewing the statistics can figure it out themselves)
for (int i = 0; i < takeoverAIs.Count; i++)
{
takeoverAIs[i].SawEnd = false;
Statistics.AddPlayer(takeoverAIs[i]);
}
}
Statistics.SawCompletion = sawCompletion;
}
catch (Exception ex)
{
Logger.Log("DTAStatisticsParser: Error parsing statistics from match! Message: " + ex.ToString());
}
}
}
}
================================================
FILE: ClientCore/Statistics/GenericMatchParser.cs
================================================
namespace ClientCore.Statistics
{
public abstract class GenericMatchParser
{
public MatchStatistics Statistics {get; set;}
public GenericMatchParser(MatchStatistics ms)
{
Statistics = ms;
}
protected abstract void ParseStatistics(string gamepath);
}
}
================================================
FILE: ClientCore/Statistics/GenericStatisticsManager.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
namespace ClientCore.Statistics
{
public abstract class GenericStatisticsManager
{
protected List Statistics = new List();
protected static string GetStatDatabaseVersion(string scorePath)
{
if (!File.Exists(scorePath))
{
return null;
}
using (StreamReader reader = new StreamReader(scorePath))
{
char[] versionBuffer = new char[4];
reader.Read(versionBuffer, 0, versionBuffer.Length);
String s = new String(versionBuffer);
return s;
}
}
public abstract void ReadStatistics(string gamePath);
public int GetMatchCount() { return Statistics.Count; }
public MatchStatistics GetMatchByIndex(int index)
{
return Statistics[index];
}
}
}
================================================
FILE: ClientCore/Statistics/MatchStatistics.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using ClientCore.Statistics.GameParsers;
using Rampastring.Tools;
namespace ClientCore.Statistics
{
public class MatchStatistics
{
public MatchStatistics() { }
public MatchStatistics(string gameVersion, int gameId, string mapName, string gameMode, int numHumans, bool mapIsCoop = false)
{
GameVersion = gameVersion;
GameID = gameId;
DateAndTime = DateTime.Now;
MapName = mapName;
GameMode = gameMode;
NumberOfHumanPlayers = numHumans;
MapIsCoop = mapIsCoop;
}
public List Players = new List();
public int LengthInSeconds { get; set; }
public DateTime DateAndTime { get; set; }
public string GameVersion { get; set; }
public string MapName { get; set; }
public string GameMode { get; set; }
public bool SawCompletion { get; set; }
public int NumberOfHumanPlayers { get; set; }
public int AverageFPS { get; set; }
public int GameID { get; set; }
public bool MapIsCoop { get; set; }
public bool IsValidForStar { get; set; } = true;
public void AddPlayer(string name, bool isLocal, bool isAI, bool isSpectator,
int side, int team, int color, int aiLevel)
{
PlayerStatistics ps = new PlayerStatistics(name, isLocal, isAI, isSpectator,
side, team, color, aiLevel);
Players.Add(ps);
}
public void AddPlayer(PlayerStatistics ps)
{
Players.Add(ps);
}
public void ParseStatistics(string gamePath, string gameName, bool isLoadedGame)
{
Logger.Log("Parsing game statistics.");
LengthInSeconds = (int)(DateTime.Now - DateAndTime).TotalSeconds;
var parser = new LogFileStatisticsParser(this, isLoadedGame);
parser.ParseStats(gamePath, ClientConfiguration.Instance.StatisticsLogFileName);
}
public PlayerStatistics GetEmptyPlayerByName(string playerName)
{
foreach (PlayerStatistics ps in Players)
{
if (ps.Name == playerName && ps.Losses == 0 && ps.Score == 0)
return ps;
}
return null;
}
public PlayerStatistics GetFirstEmptyPlayer()
{
foreach (PlayerStatistics ps in Players)
{
if (ps.Losses == 0 && ps.Score == 0)
return ps;
}
return null;
}
public int GetPlayerCount()
{
return Players.Count;
}
public PlayerStatistics GetPlayer(int index)
{
return Players[index];
}
public void Write(Stream stream)
{
// Game length
stream.WriteInt(LengthInSeconds);
// Game version, 8 bytes, ASCII
stream.WriteString(GameVersion, 8, Encoding.ASCII);
// Date and time, 8 bytes
stream.WriteLong(DateAndTime.ToBinary());
// SawCompletion, 1 byte
stream.WriteBool(SawCompletion);
// Number of players, 1 byte
stream.WriteByte(Convert.ToByte(GetPlayerCount()));
// Average FPS, 4 bytes
stream.WriteInt(AverageFPS);
// Map name, 128 bytes (64 chars), Unicode
stream.WriteString(MapName, 128);
// Game mode, 64 bytes (32 chars), Unicode
stream.WriteString(GameMode, 64);
// Unique game ID, 4 bytes
stream.WriteInt(GameID);
// Whether game options were valid for earning a star, 1 byte
stream.WriteBool(IsValidForStar);
// Write player info
for (int i = 0; i < GetPlayerCount(); i++)
{
PlayerStatistics ps = GetPlayer(i);
ps.Write(stream);
}
}
}
}
================================================
FILE: ClientCore/Statistics/PlayerStatistics.cs
================================================
using System;
using System.IO;
namespace ClientCore.Statistics
{
public class PlayerStatistics
{
public PlayerStatistics() { }
public PlayerStatistics(string name, bool isLocal, bool isAi, bool isSpectator,
int side, int team, int color, int aiLevel)
{
Name = name;
IsLocalPlayer = isLocal;
IsAI = isAi;
WasSpectator = isSpectator;
Side = side;
Team = team;
Color = color;
AILevel = aiLevel;
}
public string Name { get; set; }
public int Kills { get; set; }
public int Losses { get; set; }
public int Economy { get; set; }
public int Score { get; set; }
public int Side { get; set; }
public int Team { get; set; }
public int AILevel { get; set; }
public bool SawEnd { get; set; }
public bool WasSpectator { get; set; }
public bool Won { get; set; }
public bool IsLocalPlayer { get; set; }
public bool IsAI { get; set; }
public int Color { get; set; } = 255;
public void Write(Stream stream)
{
stream.WriteInt(Economy);
// 1 byte for IsAI
stream.WriteBool(IsAI);
// 1 byte for IsLocalPlayer
stream.WriteBool(IsLocalPlayer);
// 4 bytes for kills
stream.WriteInt(Kills);
// 4 bytes for losses
stream.WriteInt(Losses);
// Name takes 32 bytes
stream.WriteString(Name, 32);
// 1 byte for SawEnd
stream.WriteBool(SawEnd);
// 4 bytes for Score
stream.WriteInt(Score);
// 1 byte for Side
stream.WriteByte(Convert.ToByte(Side));
// 1 byte for Team
stream.WriteByte(Convert.ToByte(Team));
// 1 byte color Color
stream.WriteByte(Convert.ToByte(Color));
// 1 byte for WasSpectator
stream.WriteBool(WasSpectator);
// 1 byte for Won
stream.WriteBool(Won);
// 1 byte for AI level
stream.WriteByte(Convert.ToByte(AILevel));
}
}
}
================================================
FILE: ClientCore/Statistics/StatisticsManager.cs
================================================
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Linq;
using Rampastring.Tools;
using System.Diagnostics;
namespace ClientCore.Statistics
{
public class StatisticsManager : GenericStatisticsManager
{
private const string VERSION = "1.06";
private const string SCORE_FILE_PATH = "Client/dscore.dat";
private const string OLD_SCORE_FILE_PATH = "dscore.dat";
private static StatisticsManager _instance;
private bool _statisticsInitialized = false;
public event EventHandler GameAdded;
public static StatisticsManager Instance
{
get
{
if (_instance == null)
_instance = new StatisticsManager();
return _instance;
}
}
public override void ReadStatistics(string gamePath)
{
FileInfo scoreFileInfo = SafePath.GetFile(gamePath, SCORE_FILE_PATH);
if (!scoreFileInfo.Exists)
{
Logger.Log("Skipping reading statistics because the file doesn't exist!");
_statisticsInitialized = true;
return;
}
Logger.Log("Reading statistics.");
Statistics.Clear();
FileInfo oldScoreFileInfo = SafePath.GetFile(gamePath, OLD_SCORE_FILE_PATH);
bool resave = ReadFile(oldScoreFileInfo.FullName);
bool resaveNew = ReadFile(scoreFileInfo.FullName);
PurgeStats();
if (resave || resaveNew)
{
if (oldScoreFileInfo.Exists)
{
File.Copy(oldScoreFileInfo.FullName, SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "dscore_old.dat"));
SafePath.DeleteFileIfExists(oldScoreFileInfo.FullName);
}
SaveDatabase();
}
_statisticsInitialized = true;
}
///
/// Reads a statistics file.
///
/// The path to the statistics file.
/// A bool that determines whether the database should be re-saved.
private bool ReadFile(string filePath)
{
bool returnValue = false;
try
{
string databaseVersion = GetStatDatabaseVersion(filePath);
if (databaseVersion == null)
return false; // No score database exists
switch (databaseVersion)
{
case "1.00":
case "1.01":
ReadDatabase(filePath, 0);
returnValue = true;
break;
case "1.02":
ReadDatabase(filePath, 2);
returnValue = true;
break;
case "1.03":
ReadDatabase(filePath, 3);
returnValue = true;
break;
case "1.04":
ReadDatabase(filePath, 4);
returnValue = true;
break;
case "1.05":
ReadDatabase(filePath, 5);
returnValue = true;
break;
case "1.06":
ReadDatabase(filePath, 6);
break;
default:
throw new InvalidDataException("Invalid version for " + filePath + ": " + databaseVersion);
}
}
catch (Exception ex)
{
Logger.Log("Error reading statistics: " + ex.ToString());
}
return returnValue;
}
private void ReadDatabase(string filePath, int version)
{
// TODO split this function with the MatchStatistics and PlayerStatistics classes
try
{
using (FileStream fs = File.OpenRead(filePath))
{
fs.Position = 4; // Skip version
byte[] readBuffer = new byte[128];
fs.Read(readBuffer, 0, 4); // First 4 bytes following the version mean the amount of games
int gameCount = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
for (int i = 0; i < gameCount; i++)
{
MatchStatistics ms = new MatchStatistics();
// First 4 bytes of game info is the length in seconds
fs.Read(readBuffer, 0, 4);
int lengthInSeconds = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
ms.LengthInSeconds = lengthInSeconds;
// Next 8 are the game version
fs.Read(readBuffer, 0, 8);
ms.GameVersion = System.Text.Encoding.ASCII.GetString(readBuffer, 0, 8);
// Then comes the date and time, also 8 bytes
fs.Read(readBuffer, 0, 8);
long dateData = BinaryPrimitives.ReadInt64LittleEndian(readBuffer);
ms.DateAndTime = DateTime.FromBinary(dateData);
// Then one byte for SawCompletion
fs.Read(readBuffer, 0, 1);
ms.SawCompletion = Convert.ToBoolean(readBuffer[0]);
// Then 1 byte for the amount of players
fs.Read(readBuffer, 0, 1);
int playerCount = readBuffer[0];
if (version > 0)
{
// 4 bytes for average FPS
fs.Read(readBuffer, 0, 4);
ms.AverageFPS = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
}
int mapNameLength = 64;
if (version > 3)
{
mapNameLength = 128;
}
// Map name, 64 or 128 bytes of Unicode depending on version
fs.Read(readBuffer, 0, mapNameLength);
ms.MapName = Encoding.Unicode.GetString(readBuffer).Replace("\0", "");
// Game mode, 64 bytes
fs.Read(readBuffer, 0, 64);
ms.GameMode = Encoding.Unicode.GetString(readBuffer, 0, 64).Replace("\0", "");
if (version > 2)
{
// Unique game ID, 32 bytes (int32)
fs.Read(readBuffer, 0, 4);
ms.GameID = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
}
if (version > 5)
{
fs.Read(readBuffer, 0, 1);
ms.IsValidForStar = Convert.ToBoolean(readBuffer[0]);
}
// Player info comes right after the general match info
for (int j = 0; j < playerCount; j++)
{
PlayerStatistics ps = new PlayerStatistics();
if (version > 4)
{
// Economy is shared for the Built stat in YR
fs.Read(readBuffer, 0, 4);
ps.Economy = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
}
else
{
// Economy is between 0 and 100 in old versions, so it takes only one byte
fs.Read(readBuffer, 0, 1);
ps.Economy = readBuffer[0];
}
// IsAI is a bool, so obviously one byte
fs.Read(readBuffer, 0, 1);
ps.IsAI = Convert.ToBoolean(readBuffer[0]);
// IsLocalPlayer is also a bool
fs.Read(readBuffer, 0, 1);
ps.IsLocalPlayer = Convert.ToBoolean(readBuffer[0]);
// Kills take 4 bytes
fs.Read(readBuffer, 0, 4);
ps.Kills = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
// Losses also take 4 bytes
fs.Read(readBuffer, 0, 4);
ps.Losses = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
// 32 bytes for the name
fs.Read(readBuffer, 0, 32);
ps.Name = System.Text.Encoding.Unicode.GetString(readBuffer, 0, 32);
ps.Name = ps.Name.Replace("\0", String.Empty);
// 1 byte for SawEnd
fs.Read(readBuffer, 0, 1);
ps.SawEnd = Convert.ToBoolean(readBuffer[0]);
// 4 bytes for Score
fs.Read(readBuffer, 0, 4);
ps.Score = BinaryPrimitives.ReadInt32LittleEndian(readBuffer);
// 1 byte for Side
fs.Read(readBuffer, 0, 1);
ps.Side = readBuffer[0];
// 1 byte for Team
fs.Read(readBuffer, 0, 1);
ps.Team = readBuffer[0];
if (version > 2)
{
// 1 byte for Color
fs.Read(readBuffer, 0, 1);
ps.Color = readBuffer[0];
}
// 1 byte for WasSpectator
fs.Read(readBuffer, 0, 1);
ps.WasSpectator = Convert.ToBoolean(readBuffer[0]);
// 1 byte for Won
fs.Read(readBuffer, 0, 1);
ps.Won = Convert.ToBoolean(readBuffer[0]);
// 1 byte for AI level
fs.Read(readBuffer, 0, 1);
ps.AILevel = readBuffer[0];
ms.AddPlayer(ps);
if (!ps.IsAI)
ms.NumberOfHumanPlayers++;
}
if (ms.Players.Find(p => p.IsLocalPlayer && !p.IsAI) == null)
continue;
Statistics.Add(ms);
}
}
}
catch (Exception ex)
{
Logger.Log("Reading the statistics file failed! Message: " + ex.ToString());
}
}
public void PurgeStats()
{
int removedCount = 0;
for (int i = 0; i < Statistics.Count; i++)
{
if (Statistics[i].LengthInSeconds < 60)
{
Logger.Log("Removing match on " + Statistics[i].MapName + " because it's too short.");
Statistics.RemoveAt(i);
i--;
removedCount++;
}
}
if (removedCount > 0)
SaveDatabase();
}
public void ClearDatabase()
{
Statistics.Clear();
CreateDummyFile();
_statisticsInitialized = true;
}
public void AddMatchAndSaveDatabase(bool addMatch, MatchStatistics ms)
{
if (ms == null)
{
Logger.Log("Skipping adding match to statistics because match statistics is null.");
return;
}
// Skip adding stats if the game only had one player, make exception for co-op since it doesn't recognize pre-placed houses as players.
if (ms.GetPlayerCount() <= 1 && !ms.MapIsCoop)
{
Logger.Log("Skipping adding match to statistics because game only had one player.");
return;
}
if (ms.LengthInSeconds < 60)
{
Logger.Log("Skipping adding match to statistics because the game was cancelled.");
return;
}
if (addMatch)
{
Statistics.Add(ms);
GameAdded?.Invoke(this, EventArgs.Empty);
}
FileInfo scoreFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH);
if (!scoreFileInfo.Exists)
{
CreateDummyFile();
}
Logger.Log("Writing game info to statistics file.");
using (FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite))
{
fs.Position = 4; // First 4 bytes after the version mean the amount of games
fs.WriteInt(Statistics.Count);
fs.Position = fs.Length;
ms.Write(fs);
}
Logger.Log("Finished writing statistics.");
}
private void CreateDummyFile()
{
Logger.Log("Creating empty statistics file.");
using StreamWriter sw = new StreamWriter(SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH).Create());
sw.Write(VERSION);
}
///
/// Deletes the statistics file on the file system and rewrites it.
///
public void SaveDatabase()
{
FileInfo scoreFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH);
SafePath.DeleteFileIfExists(scoreFileInfo.FullName);
CreateDummyFile();
using (FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite))
{
fs.Position = 4; // First 4 bytes after the version mean the amount of games
fs.WriteInt(Statistics.Count);
foreach (MatchStatistics ms in Statistics)
{
ms.Write(fs);
}
}
}
public bool HasBeatCoOpMap(string mapName, string gameMode)
{
Debug.Assert(_statisticsInitialized, "StatisticsManager must have been initialized before.");
List matches = new List();
// Filter out unfitting games
foreach (MatchStatistics ms in Statistics)
{
if (ms.SawCompletion &&
ms.MapName == mapName &&
ms.GameMode == gameMode)
{
if (ms.Players[0].Won)
return true;
}
}
return false;
}
public int GetCoopRankForDefaultMap(string mapName, int requiredPlayerCount)
{
Debug.Assert(_statisticsInitialized, "StatisticsManager must have been initialized before.");
List matches = new List();
// Filter out unfitting games
foreach (MatchStatistics ms in Statistics)
{
if (!ms.SawCompletion)
continue;
if (!ms.IsValidForStar)
continue;
if (ms.MapName != mapName)
continue;
if (ms.Players.Count != requiredPlayerCount)
continue;
if (ms.Players.Count(ps => !ps.IsAI && !ps.WasSpectator) > 1 &&
ms.Players.Find(ps => ps.IsAI) != null)
matches.Add(ms);
}
int rank = -1;
foreach (MatchStatistics ms in matches)
{
rank = Math.Max(rank, GetRankForCoopMatch(ms));
}
return rank;
}
int GetRankForCoopMatch(MatchStatistics ms)
{
PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer);
if (localPlayer == null || !localPlayer.Won)
return -1;
if (ms.Players.Find(p => p.WasSpectator) != null)
return -1; // Don't allow matches with spectators
if (ms.Players.Count(p => !p.IsAI && p.Team != localPlayer.Team) > 0)
return -1; // Don't allow matches with human players who were on a different team
if (ms.Players.Find(p => p.Team == 0) != null)
return -1; // Matches with non-allied players are discarded
if (ms.Players.All(ps => ps.Team == localPlayer.Team))
return -1; // Discard matches that had no enemies
int[] teamMemberCounts = new int[5];
int lowestEnemyAILevel = 2;
int highestAllyAILevel = 0;
for (int i = 0; i < ms.Players.Count; i++)
{
PlayerStatistics ps = ms.GetPlayer(i);
teamMemberCounts[ps.Team]++;
if (!ps.IsAI)
{
continue;
}
if (ps.Team > 0 && ps.Team == localPlayer.Team)
{
if (ps.AILevel > highestAllyAILevel)
highestAllyAILevel = ps.AILevel;
}
else
{
if (ps.AILevel < lowestEnemyAILevel)
lowestEnemyAILevel = ps.AILevel;
}
}
if (lowestEnemyAILevel < highestAllyAILevel)
{
// Check that the player's AI allies weren't stronger
return -1;
}
// Check that all teams had at least as many players
// as the local player's team
int allyCount = teamMemberCounts[localPlayer.Team];
for (int i = 1; i < 5; i++)
{
if (i == localPlayer.Team)
continue;
if (teamMemberCounts[i] > 0)
{
if (teamMemberCounts[i] < allyCount)
return -1;
}
}
return lowestEnemyAILevel;
}
public bool HasWonMapInPvP(string mapName, string gameMode, int requiredPlayerCount)
{
Debug.Assert(_statisticsInitialized, "StatisticsManager must have been initialized before.");
List matches = new List();
foreach (MatchStatistics ms in Statistics)
{
if (!ms.SawCompletion)
continue;
if (!ms.IsValidForStar)
continue;
if (ms.MapName != mapName)
continue;
if (ms.GameMode != gameMode)
continue;
if (ms.Players.Count(ps => !ps.WasSpectator) != requiredPlayerCount)
continue;
if (ms.Players.Find(ps => ps.IsAI) != null)
continue;
PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer);
if (localPlayer == null)
continue;
if (localPlayer.WasSpectator)
continue;
if (!localPlayer.Won)
continue;
int[] teamMemberCounts = new int[5];
ms.Players.FindAll(ps => !ps.WasSpectator).ForEach(ps => teamMemberCounts[ps.Team]++);
if (localPlayer.Team > 0)
{
int lowestEnemyTeamMemberCount = int.MaxValue;
for (int i = 1; i < 5; i++)
{
if (i != localPlayer.Team && teamMemberCounts[i] > 0)
{
if (teamMemberCounts[i] < lowestEnemyTeamMemberCount)
lowestEnemyTeamMemberCount = teamMemberCounts[i];
}
}
if (lowestEnemyTeamMemberCount > teamMemberCounts[localPlayer.Team])
continue;
return true;
}
if (ms.Players.Count(ps => !ps.WasSpectator) > 1)
return true;
}
return false;
}
public int GetSkirmishRankForDefaultMap(string mapName, int requiredPlayerCount)
{
Debug.Assert(_statisticsInitialized, "StatisticsManager must have been initialized before.");
List matches = new List();
// Filter out unfitting games
foreach (MatchStatistics ms in Statistics)
{
if (ms.SawCompletion &&
ms.IsValidForStar &&
ms.MapName == mapName &&
ms.Players.Count == requiredPlayerCount &&
ms.Players.Count(p => !p.IsAI) == 1)
matches.Add(ms);
}
int rank = -1;
foreach (MatchStatistics ms in matches)
{
// TODO This code turned out pretty ugly, should design it better
PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer);
if (localPlayer == null || !localPlayer.Won)
continue;
int[] teamMemberCounts = new int[5];
int lowestEnemyAILevel = 2;
int highestAllyAILevel = 0;
for (int i = 0; i < ms.Players.Count; i++)
{
PlayerStatistics ps = ms.GetPlayer(i);
teamMemberCounts[ps.Team]++;
if (ps.IsLocalPlayer)
{
continue;
}
if (ps.Team > 0 && ps.Team == localPlayer.Team)
{
if (ps.AILevel > highestAllyAILevel)
highestAllyAILevel = ps.AILevel;
}
else
{
if (ps.AILevel < lowestEnemyAILevel)
lowestEnemyAILevel = ps.AILevel;
}
}
if (lowestEnemyAILevel < highestAllyAILevel)
{
// Check that the player's AI allies weren't stronger
continue;
}
if (localPlayer.Team > 0)
{
// Check that all teams had at least as many players as the human player's team
int allyCount = teamMemberCounts[localPlayer.Team];
bool pass = true;
for (int i = 1; i < 5; i++)
{
if (i == localPlayer.Team)
continue;
if (teamMemberCounts[i] > 0)
{
if (teamMemberCounts[i] < allyCount)
{
// The enemy team has fewer players than the player's team
pass = false;
break;
}
}
}
if (!pass)
continue;
// Check that there is a team other than the players' team that is at least as large
pass = false;
for (int i = 1; i < 5; i++)
{
if (i == localPlayer.Team)
continue;
if (teamMemberCounts[i] >= allyCount)
{
pass = true;
break;
}
}
if (!pass)
continue;
}
if (rank < lowestEnemyAILevel)
{
rank = lowestEnemyAILevel;
if (rank == 2)
return rank; // Best possible rank
}
}
return rank;
}
public bool IsGameIdUnique(int gameId)
{
Debug.Assert(_statisticsInitialized, "StatisticsManager must have been initialized before.");
return Statistics.Find(m => m.GameID == gameId) == null;
}
public MatchStatistics GetMatchWithGameID(int gameId)
{
Debug.Assert(_statisticsInitialized, "StatisticsManager must have been initialized before.");
return Statistics.Find(m => m.GameID == gameId);
}
}
}
================================================
FILE: ClientGUI/ClientGUI.csproj
================================================
CnCNet Client UI Library
================================================
FILE: ClientGUI/ClientGUICreator.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Rampastring.XNAUI.XNAControls;
namespace ClientGUI
{
///
/// This gui creator helps in the registration of XNAControl based controls that can be used via dependency injection
/// or through the INI system.
///
public static class ClientGUICreator
{
private static List controlTypes = new();
private static IServiceProvider serviceProvider;
///
/// Adds a control type as a singleton to our list of known control types.
///
/// When a control is added as singleton, the same instance will be returned every time one is requested by the control's name.
///
/// Service collection for our dependency injection.
/// The control type to add.
/// IServiceCollection.
public static IServiceCollection AddSingletonXnaControl(this IServiceCollection serviceCollection)
{
Type controlType = typeof(T);
AddXnaControl(controlType);
return serviceCollection.AddSingleton(controlType, provider => GetXnaControl(provider, controlType.Name));
}
///
/// Adds a control type as a transient to our list of known control types.
///
/// When a control is added as transient, a new instance will be instantiated every time one is requested by the control's name.
///
/// Service collection for our dependency injection.
/// The control type to add.
/// IServiceCollection.
public static IServiceCollection AddTransientXnaControl(this IServiceCollection serviceCollection)
{
Type controlType = typeof(T);
AddXnaControl(controlType);
return serviceCollection.AddTransient(controlType, provider => GetXnaControl(provider, controlType.Name));
}
///
/// This is typically called during control initialization via the INI UI system.
///
/// The name of the control to instantiate.
/// XNAControl instance.
public static XNAControl GetXnaControl(string controlTypeName) => GetXnaControl(serviceProvider, controlTypeName);
///
/// Adds the control type to our list of known controls for instantiation.
///
/// The control type to add.
///
/// If this control is not a sub-class of XNAControl or is not an XNAControl itself.
/// OR, this component type is added more than once.
///
private static void AddXnaControl(Type controlType)
{
if (!controlType.IsSubclassOf(typeof(XNAControl)) && controlType != typeof(XNAControl))
throw new Exception($"{controlType.Name} is not a sub class of {nameof(XNAControl)}");
ValidateNonDuplicateControlType(controlType);
controlTypes.Add(controlType);
}
///
/// Because the INI system retrieves controls by its , we need to make sure that
/// duplicates are not being registered with the same base name as another control.
///
/// The Type to validate.
/// If another control was registered with the same name.
private static void ValidateNonDuplicateControlType(Type controlType)
{
if (controlTypes.Any(c => c.Name == controlType.Name))
throw new Exception($"A control type with name {controlType.Name} has already been registered.");
}
///
/// This is the "factory" that is used to instantiate a control.
///
/// If this function is called for a singleton, it will only be called ONCE for a given
///
/// Our dependency injection service provider.
/// The name of the control type to instantiate.
/// XNAControl instance.
/// If the control type was not registered with our service provider.
private static XNAControl GetXnaControl(IServiceProvider provider, string controlTypeName)
{
serviceProvider ??= provider;
Type controlType = controlTypes.SingleOrDefault(control => control.Name == controlTypeName);
if (controlType == null)
throw new Exception($"Control type {controlTypeName} was not registered with ServiceCollection in GameClass");
ConstructorInfo constructor = controlType.GetConstructors().First();
IEnumerable