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 parameterInstances = constructor.GetParameters().Select(param => GetTypeInstance(param.ParameterType)); return (XNAControl)constructor.Invoke(parameterInstances.ToArray()); } /// /// Attempts to get an instance of a specific type from our serviced provider. /// /// The type to instantiate. /// An instance of the type specified. /// If the type was not registered with our service provider. private static object GetTypeInstance(Type type) => serviceProvider.GetService(type) ?? throw new Exception($"Control type {type.Name} was not registered with ServiceCollection in GameClass"); } } ================================================ FILE: ClientGUI/DarkeningPanel.cs ================================================ using Rampastring.XNAUI.XNAControls; using System; using Rampastring.XNAUI; using Microsoft.Xna.Framework; namespace ClientGUI { /// /// A panel that darkens the whole screen. /// public class DarkeningPanel : XNAPanel { public const float ALPHA_RATE = 0.6f; private bool _fadeEnabled = true; public DarkeningPanel(WindowManager windowManager) : base(windowManager) { DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; } public event EventHandler Hidden; public override void Initialize() { Name = "DarkeningPanel"; SetPositionAndSize(); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); DrawBorders = false; base.Initialize(); } public void SetPositionAndSize() { if (Parent != null) { ClientRectangle = new Rectangle(-Parent.X, -Parent.Y, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY); } else { ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY); } } public override void AddChild(XNAControl child) { base.AddChild(child); child.VisibleChanged += Child_VisibleChanged; } private void Child_VisibleChanged(object sender, EventArgs e) { var xnaControl = (XNAControl)sender; if (xnaControl.Visible) Show(); else Hide(); } public void Show() { Enabled = true; Visible = true; if (_fadeEnabled) { AlphaRate = ALPHA_RATE; Alpha = 0.01f; } else { AlphaRate = 1.0f; Alpha = 1.0f; } foreach (XNAControl child in Children) { child.Enabled = true; child.Visible = true; } } public void Hide() { if (_fadeEnabled) { AlphaRate = -ALPHA_RATE; } else { Enabled = false; Visible = false; Hidden?.Invoke(this, EventArgs.Empty); } foreach (XNAControl child in Children) { child.Enabled = false; child.Visible = false; } } public override void Update(GameTime gameTime) { base.Update(gameTime); if (Alpha <= 0.0f) { Enabled = false; Visible = false; Hidden?.Invoke(this, EventArgs.Empty); } } public static void AddAndInitializeWithControl(WindowManager wm, XNAControl control) { var dp = new DarkeningPanel(wm); wm.AddAndInitializeControl(dp); dp.AddChild(control); } public void ToggleFade(bool enabled) { _fadeEnabled = enabled; } } } ================================================ FILE: ClientGUI/GameProcessLogic.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using ClientCore; using Rampastring.Tools; using ClientCore.INIProcessing; using System.Threading; using Rampastring.XNAUI; using ClientCore.Extensions; namespace ClientGUI { /// /// A static class used for controlling the launching and exiting of the game executable. /// public static class GameProcessLogic { public static event Action GameProcessStarted; public static event Action GameProcessStarting; public static event Action GameProcessExited; public static bool UseQres { get; set; } public static bool SingleCoreAffinity { get; set; } /// /// Starts the main game process. /// public static void StartGameProcess(WindowManager windowManager) { Logger.Log("About to launch main game executable."); // In the relatively unlikely event that INI preprocessing is still going on, just wait until it's done. // TODO ideally this should be handled in the UI so the client doesn't appear just frozen for the user. int waitTimes = 0; while (PreprocessorBackgroundTask.Instance.IsRunning) { Logger.Log("The preprocessor background task is still running. Wait for it..."); Thread.Sleep(1000); waitTimes++; if (waitTimes > 10) { XNAMessageBox.Show(windowManager, "INI preprocessing not complete".L10N("Client:ClientGUI:INIPreprocessingNotCompleteTitle"), ("INI preprocessing not complete. Please try " + "launching the game again. If the problem persists, " + "contact the game or mod authors for support.").L10N("Client:ClientGUI:INIPreprocessingNotCompleteText")); return; } } OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); string gameExecutableName; string additionalExecutableName = string.Empty; string errorLaunchingTitle = "Error launching game".L10N("Client:ClientGUI:ErrorLaunchingTitle"); string errorLaunchingText = ("Error launching {0}. Please check that your anti-virus isn't blocking the CnCNet Client. " + "You can also try running the client as an administrator.\n\nYou are unable to participate in this match. \n\n" + "Returned error: {1}").L10N("Client:ClientGUI:ErrorLaunchingText"); if (osVersion == OSVersion.UNIX) gameExecutableName = ClientConfiguration.Instance.UnixGameExecutableName; else { string launcherExecutableName = ClientConfiguration.Instance.GameLauncherExecutableName; if (string.IsNullOrEmpty(launcherExecutableName)) gameExecutableName = ClientConfiguration.Instance.GetGameExecutableName(); else { gameExecutableName = launcherExecutableName; additionalExecutableName = "\"" + ClientConfiguration.Instance.GetGameExecutableName() + "\" "; } } string extraCommandLine = ClientConfiguration.Instance.ExtraExeCommandLineParameters; SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "DTA.LOG"); SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "TI.LOG"); SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "TS.LOG"); GameProcessStarting?.Invoke(); if (UserINISettings.Instance.WindowedMode && UseQres && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Logger.Log("Windowed mode is enabled - using QRes."); Process QResProcess = new Process(); QResProcess.StartInfo.FileName = ProgramConstants.QRES_EXECUTABLE; QResProcess.StartInfo.UseShellExecute = false; if (!string.IsNullOrEmpty(extraCommandLine)) QResProcess.StartInfo.Arguments = "c=16 /R " + "\"" + SafePath.CombineFilePath(ProgramConstants.GamePath, gameExecutableName) + "\" " + additionalExecutableName + "-SPAWN " + extraCommandLine; else QResProcess.StartInfo.Arguments = "c=16 /R " + "\"" + SafePath.CombineFilePath(ProgramConstants.GamePath, gameExecutableName) + "\" " + additionalExecutableName + "-SPAWN"; QResProcess.EnableRaisingEvents = true; QResProcess.Exited += new EventHandler(Process_Exited); Logger.Log("Launch executable: " + QResProcess.StartInfo.FileName); Logger.Log("Launch arguments: " + QResProcess.StartInfo.Arguments); try { QResProcess.Start(); } catch (Exception ex) { Logger.Log("Error launching QRes: " + ex.ToString()); XNAMessageBox.Show(windowManager, errorLaunchingTitle, string.Format(errorLaunchingText, ProgramConstants.QRES_EXECUTABLE, ex.Message)); Process_Exited(QResProcess, EventArgs.Empty); return; } if (Environment.ProcessorCount > 1 && SingleCoreAffinity) QResProcess.ProcessorAffinity = (IntPtr)2; } else { string arguments; if (!string.IsNullOrWhiteSpace(extraCommandLine)) arguments = " " + additionalExecutableName + "-SPAWN " + extraCommandLine; else arguments = additionalExecutableName + "-SPAWN"; FileInfo gameFileInfo = SafePath.GetFile(ProgramConstants.GamePath, gameExecutableName); var gameProcess = new Process(); gameProcess.StartInfo.FileName = gameFileInfo.FullName; gameProcess.StartInfo.Arguments = arguments; gameProcess.StartInfo.UseShellExecute = false; gameProcess.EnableRaisingEvents = true; gameProcess.Exited += Process_Exited; Logger.Log("Launch executable: " + gameProcess.StartInfo.FileName); Logger.Log("Launch arguments: " + gameProcess.StartInfo.Arguments); try { gameProcess.Start(); Logger.Log("GameProcessLogic: Process started."); } catch (Exception ex) { Logger.Log("Error launching " + gameFileInfo.Name + ": " + ex.ToString()); XNAMessageBox.Show(windowManager, errorLaunchingTitle, string.Format(errorLaunchingText, gameFileInfo.Name, ex.Message)); Process_Exited(gameProcess, EventArgs.Empty); return; } if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) && Environment.ProcessorCount > 1 && SingleCoreAffinity) { gameProcess.ProcessorAffinity = (IntPtr)2; } } GameProcessStarted?.Invoke(); Logger.Log("Waiting for qres.dat or " + gameExecutableName + " to exit."); } static void Process_Exited(object sender, EventArgs e) { Logger.Log("GameProcessLogic: Process exited."); Process proc = (Process)sender; proc.Exited -= Process_Exited; proc.Dispose(); GameProcessExited?.Invoke(); } } } ================================================ FILE: ClientGUI/HotkeyConfigurationWindow.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using ClientCore; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace ClientGUI { /// /// A window for configuring in-game hotkeys. /// public class HotkeyConfigurationWindow : XNAWindow { private readonly string HOTKEY_TIP_TEXT = "Press a key...".L10N("Client:DTAConfig:PressAKey"); private const string KEYBOARD_COMMANDS_INI = "KeyboardCommands.ini"; public HotkeyConfigurationWindow(WindowManager windowManager) : base(windowManager) { } /// /// Keys that the client doesn't allow to be used regular hotkeys. /// private readonly Keys[] keyBlacklist = new Keys[] { Keys.LeftAlt, Keys.RightAlt, Keys.LeftControl, Keys.RightControl, Keys.LeftShift, Keys.RightShift }; private readonly List gameCommands = new List(); private XNAClientDropDown ddCategory = null!; private XNAMultiColumnListBox lbHotkeys = null!; private XNAPanel hotkeyInfoPanel = null!; private XNALabel lblCommandCaption = null!; private XNALabel lblDescription = null!; private XNALabel lblCurrentHotkeyValue = null!; private XNALabel lblNewHotkeyValue = null!; private XNALabel lblCurrentlyAssignedTo = null!; private XNALabel lblDefaultHotkeyValue = null!; private XNAClientButton btnResetKey = null!; private Hotkey pendingHotkey = Hotkey.None; private KeyModifiers lastFrameModifiers; public override void Initialize() { ReadGameCommands(); Name = "HotkeyConfigurationWindow"; ClientRectangle = new Rectangle(0, 0, 600, 450); BackgroundTexture = AssetLoader.LoadTextureUncached("hotkeyconfigbg.png"); var lblCategory = new XNALabel(WindowManager); lblCategory.Name = "lblCategory"; lblCategory.ClientRectangle = new Rectangle(12, 12, 0, 0); lblCategory.Text = "Category:".L10N("Client:DTAConfig:Category"); ddCategory = new XNAClientDropDown(WindowManager); ddCategory.Name = "ddCategory"; ddCategory.ClientRectangle = new Rectangle(lblCategory.Right + 12, lblCategory.Y - 1, 250, ddCategory.Height); HashSet categories = new HashSet(); foreach (var command in gameCommands) { if (!categories.Contains(command.Category)) categories.Add(command.Category); } foreach (string category in categories) ddCategory.AddItem(category); lbHotkeys = new XNAMultiColumnListBox(WindowManager); lbHotkeys.Name = "lbHotkeys"; lbHotkeys.ClientRectangle = new Rectangle(12, ddCategory.Bottom + 12, ddCategory.Right - 12, Height - ddCategory.Bottom - 59); lbHotkeys.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbHotkeys.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbHotkeys.AddColumn("Command".L10N("Client:DTAConfig:Command"), 150); lbHotkeys.AddColumn("Shortcut".L10N("Client:DTAConfig:Shortcut"), lbHotkeys.Width - 150); hotkeyInfoPanel = new XNAPanel(WindowManager); hotkeyInfoPanel.Name = "HotkeyInfoPanel"; hotkeyInfoPanel.ClientRectangle = new Rectangle(lbHotkeys.Right + 12, ddCategory.Y, Width - lbHotkeys.Right - 24, lbHotkeys.Height + ddCategory.Height + 12); lblCommandCaption = new XNALabel(WindowManager); lblCommandCaption.Name = "lblCommandCaption"; lblCommandCaption.FontIndex = 1; lblCommandCaption.ClientRectangle = new Rectangle(12, 12, 0, 0); lblCommandCaption.Text = "Command name".L10N("Client:DTAConfig:CommandName"); lblDescription = new XNALabel(WindowManager); lblDescription.Name = "lblDescription"; lblDescription.ClientRectangle = new Rectangle(12, lblCommandCaption.Bottom + 12, 0, 0); lblDescription.Text = "Command description".L10N("Client:DTAConfig:CommandDescription"); var lblCurrentHotkey = new XNALabel(WindowManager); lblCurrentHotkey.Name = "lblCurrentHotkey"; lblCurrentHotkey.ClientRectangle = new Rectangle(lblDescription.X, lblDescription.Bottom + 48, 0, 0); lblCurrentHotkey.FontIndex = 1; lblCurrentHotkey.Text = "Currently assigned hotkey:".L10N("Client:DTAConfig:CurrentHotKey"); lblCurrentHotkeyValue = new XNALabel(WindowManager); lblCurrentHotkeyValue.Name = "lblCurrentHotkeyValue"; lblCurrentHotkeyValue.ClientRectangle = new Rectangle(lblDescription.X, lblCurrentHotkey.Bottom + 6, 0, 0); lblCurrentHotkeyValue.Text = "Current hotkey value".L10N("Client:DTAConfig:CurrentHotKeyValue"); var lblNewHotkey = new XNALabel(WindowManager); lblNewHotkey.Name = "lblNewHotkey"; lblNewHotkey.ClientRectangle = new Rectangle(lblDescription.X, lblCurrentHotkeyValue.Bottom + 48, 0, 0); lblNewHotkey.FontIndex = 1; lblNewHotkey.Text = "New hotkey:".L10N("Client:DTAConfig:NewHotKey"); lblNewHotkeyValue = new XNALabel(WindowManager); lblNewHotkeyValue.Name = "lblNewHotkeyValue"; lblNewHotkeyValue.ClientRectangle = new Rectangle(lblDescription.X, lblNewHotkey.Bottom + 6, 0, 0); lblNewHotkeyValue.Text = HOTKEY_TIP_TEXT; lblCurrentlyAssignedTo = new XNALabel(WindowManager); lblCurrentlyAssignedTo.Name = "lblCurrentlyAssignedTo"; lblCurrentlyAssignedTo.ClientRectangle = new Rectangle(lblDescription.X, lblNewHotkeyValue.Bottom + 12, 0, 0); lblCurrentlyAssignedTo.Text = "Currently assigned to:".L10N("Client:DTAConfig:CurrentHotKeyAssign") + "\nKey"; var btnAssign = new XNAClientButton(WindowManager); btnAssign.Name = "btnAssign"; btnAssign.ClientRectangle = new Rectangle(lblDescription.X, lblCurrentlyAssignedTo.Bottom + 24, UIDesignConstants.BUTTON_WIDTH_121, UIDesignConstants.BUTTON_HEIGHT); btnAssign.Text = "Assign Hotkey".L10N("Client:DTAConfig:AssignHotkey"); btnAssign.LeftClick += BtnAssign_LeftClick; btnResetKey = new XNAClientButton(WindowManager); btnResetKey.Name = "btnResetKey"; btnResetKey.ClientRectangle = new Rectangle(btnAssign.X, btnAssign.Bottom + 12, btnAssign.Width, 23); btnResetKey.Text = "Reset to Default".L10N("Client:DTAConfig:ResetToDefault"); btnResetKey.LeftClick += BtnReset_LeftClick; var lblDefaultHotkey = new XNALabel(WindowManager); lblDefaultHotkey.Name = "lblOriginalHotkey"; lblDefaultHotkey.ClientRectangle = new Rectangle(lblCurrentHotkey.X, btnResetKey.Bottom + 12, 0, 0); lblDefaultHotkey.Text = "Default hotkey:".L10N("Client:DTAConfig:DefaultHotKey"); lblDefaultHotkeyValue = new XNALabel(WindowManager); lblDefaultHotkeyValue.Name = "lblDefaultHotkeyValue"; lblDefaultHotkeyValue.ClientRectangle = new Rectangle(lblDefaultHotkey.Right + 12, lblDefaultHotkey.Y, 0, 0); var btnSave = new XNAClientButton(WindowManager); btnSave.Name = "btnSave"; btnSave.ClientRectangle = new Rectangle(12, lbHotkeys.Bottom + 12, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnSave.Text = "Save".L10N("Client:DTAConfig:ButtonSave"); btnSave.LeftClick += BtnSave_LeftClick; var btnResetAllKeys = new XNAClientButton(WindowManager); btnResetAllKeys.Name = "btnResetAllToDefaults"; btnResetAllKeys.ClientRectangle = new Rectangle(0, btnSave.Y, UIDesignConstants.BUTTON_WIDTH_121, UIDesignConstants.BUTTON_HEIGHT); btnResetAllKeys.Text = "Reset All Keys".L10N("Client:DTAConfig:ResetAllHotkey"); btnResetAllKeys.LeftClick += BtnResetToDefaults_LeftClick; AddChild(btnResetAllKeys); btnResetAllKeys.CenterOnParentHorizontally(); var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = "btnExit"; btnCancel.ClientRectangle = new Rectangle(Width - 104, btnSave.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:DTAConfig:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(lbHotkeys); AddChild(lblCategory); AddChild(ddCategory); AddChild(hotkeyInfoPanel); AddChild(btnSave); AddChild(btnCancel); hotkeyInfoPanel.AddChild(lblCommandCaption); hotkeyInfoPanel.AddChild(lblDescription); hotkeyInfoPanel.AddChild(lblCurrentHotkey); hotkeyInfoPanel.AddChild(lblCurrentHotkeyValue); hotkeyInfoPanel.AddChild(lblNewHotkey); hotkeyInfoPanel.AddChild(lblNewHotkeyValue); hotkeyInfoPanel.AddChild(lblCurrentlyAssignedTo); hotkeyInfoPanel.AddChild(lblDefaultHotkey); hotkeyInfoPanel.AddChild(lblDefaultHotkeyValue); hotkeyInfoPanel.AddChild(btnAssign); hotkeyInfoPanel.AddChild(btnResetKey); if (categories.Count > 0) { hotkeyInfoPanel.Disable(); lbHotkeys.SelectedIndexChanged += LbHotkeys_SelectedIndexChanged; ddCategory.SelectedIndexChanged += DdCategory_SelectedIndexChanged; ddCategory.SelectedIndex = 0; } else Logger.Log("No keyboard game commands exist!"); GameProcessLogic.GameProcessExited += GameProcessLogic_GameProcessExited; base.Initialize(); CenterOnParent(); Keyboard.OnKeyPressed += Keyboard_OnKeyPressed; EnabledChanged += HotkeyConfigurationWindow_EnabledChanged; // Load and apply the hotkeys so that if the default keyboard INI file is updated during a client update LoadKeyboardINI(); RefreshHotkeyList(); WriteKeyboardINI(writeEvenIfSettingsIniAsKeyboardIniHolds: true); } /// /// Reads game commands from an INI file. /// private void ReadGameCommands() { var gameCommandsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), KEYBOARD_COMMANDS_INI)); List sections = gameCommandsIni.GetSections(); HashSet defaultHotkeys = []; foreach (string sectionName in sections) { var gameCommand = new GameCommand(gameCommandsIni.GetSection(sectionName)); gameCommands.Add(gameCommand); // Check duplicates for default hotkeys if (gameCommand.DefaultHotkey != null && gameCommand.DefaultHotkey != Hotkey.None) { bool isDuplicate = !defaultHotkeys.Add(gameCommand.DefaultHotkey); if (isDuplicate) throw new Exception("The default hotkey " + gameCommand.DefaultHotkey.ToString() + " for command " + gameCommand.UIName + " is duplicated with another command's default hotkey. Please make sure all default hotkeys in " + KEYBOARD_COMMANDS_INI + " are unique."); } } } /// /// Resets the hotkey for the currently selected game command to its /// default value. /// private void BtnReset_LeftClick(object? sender, EventArgs e) { if (lbHotkeys.SelectedIndex < 0 || lbHotkeys.SelectedIndex >= lbHotkeys.ItemCount) { return; } var command = (GameCommand)lbHotkeys.GetItem(0, lbHotkeys.SelectedIndex).Tag; if (command.DefaultHotkey == null) { command.Hotkey = null; } else { command.Hotkey = command.DefaultHotkey; // If the hotkey is already assigned to some other command, unbind it foreach (var gameCommand in gameCommands) { if (gameCommand != command && gameCommand.Hotkey == command.Hotkey) gameCommand.Hotkey = null; } } pendingHotkey = Hotkey.None; RefreshHotkeyList(); } private void BtnResetToDefaults_LeftClick(object? sender, EventArgs e) { foreach (var command in gameCommands) { if (command.DefaultHotkey == null) command.Hotkey = null; else command.Hotkey = command.DefaultHotkey; } RefreshHotkeyList(); } private void HotkeyConfigurationWindow_EnabledChanged(object? sender, EventArgs e) { if (Enabled) { LoadKeyboardINI(); RefreshHotkeyList(); } } /// /// Reloads Keyboard.ini when the game process has exited. /// private void GameProcessLogic_GameProcessExited() { WindowManager.AddCallback(new Action(LoadKeyboardINI), null); } private void LoadKeyboardINI() { var keyboardINI = ClientConfiguration.Instance.SettingsIniAsKeyboardIni ? UserINISettings.Instance.SettingsIni : new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI)); var hotkeySection = keyboardINI.GetOrAddSection(ClientConfiguration.Instance.KeyboardHotkeySection); // Load the hotkeys from the INI file var assignedHotkeys = new HashSet(); foreach (var command in gameCommands) { int? tsHotkey = hotkeySection.GetIntValueOrNull(command.ININame); if (tsHotkey.HasValue) { Hotkey hotkey = new(tsHotkey.Value); bool isDuplicate = false; if (hotkey != Hotkey.None) isDuplicate = !assignedHotkeys.Add(hotkey); if (!isDuplicate) command.Hotkey = hotkey; else command.Hotkey = null; } else { // Clear any previously assigned hotkey when no value exists in the INI command.Hotkey = null; } } // Assign default hotkeys foreach (var command in gameCommands) { bool hotkeyAssigned = hotkeySection.KeyExists(command.ININame); if (!hotkeyAssigned && command.DefaultHotkey != null) { // Try assigning the default hotkey if it exists and is not occupied by other commands bool occupied = false; if (command.DefaultHotkey != Hotkey.None) { foreach (var otherCommand in gameCommands) { if (otherCommand != command && command.DefaultHotkey == otherCommand.Hotkey) { occupied = true; break; } } } if (!occupied) command.Hotkey = command.DefaultHotkey; } } } private void LbHotkeys_SelectedIndexChanged(object? sender, EventArgs e) { if (lbHotkeys.SelectedIndex < 0 || lbHotkeys.SelectedIndex >= lbHotkeys.ItemCount) { hotkeyInfoPanel.Disable(); return; } hotkeyInfoPanel.Enable(); var command = (GameCommand)lbHotkeys.GetItem(0, lbHotkeys.SelectedIndex).Tag; lblCommandCaption.Text = command.UIName; lblDescription.Text = Renderer.FixText(command.Description, lblDescription.FontIndex, hotkeyInfoPanel.Width - lblDescription.X).Text; lblCurrentHotkeyValue.Text = command.Hotkey?.ToStringWithNone(); lblDefaultHotkeyValue.Text = command.DefaultHotkey?.ToStringWithNone(); btnResetKey.Enabled = command.DefaultHotkey != command.Hotkey; lblNewHotkeyValue.Text = HOTKEY_TIP_TEXT; pendingHotkey = Hotkey.None; lblCurrentlyAssignedTo.Text = string.Empty; } private void DdCategory_SelectedIndexChanged(object? sender, EventArgs e) { lbHotkeys.ClearItems(); lbHotkeys.TopIndex = 0; string category = ddCategory.SelectedItem.Text; foreach (var command in gameCommands) { if (command.Category == category) { lbHotkeys.AddItem(new XNAListBoxItem[] { new XNAListBoxItem() { Text = command.UIName, Tag = command }, new XNAListBoxItem() { Text = command.Hotkey?.ToString() } }); } } lbHotkeys.SelectedIndex = -1; } private void BtnAssign_LeftClick(object? sender, EventArgs e) { if (lbHotkeys.SelectedIndex < 0 || lbHotkeys.SelectedIndex >= lbHotkeys.ItemCount) { return; } // If the hotkey is already assigned to other command, unbind it if (pendingHotkey != Hotkey.None) { foreach (var gameCommand in gameCommands) { if (pendingHotkey == gameCommand.Hotkey) gameCommand.Hotkey = null; } } var command = (GameCommand)lbHotkeys.GetItem(0, lbHotkeys.SelectedIndex).Tag; command.Hotkey = pendingHotkey; RefreshHotkeyList(); pendingHotkey = Hotkey.None; } private void RefreshHotkeyList() { int selectedIndex = lbHotkeys.SelectedIndex; int topIndex = lbHotkeys.TopIndex; DdCategory_SelectedIndexChanged(null, EventArgs.Empty); lbHotkeys.TopIndex = topIndex; lbHotkeys.SelectedIndex = selectedIndex; } /// /// Detects when the user has pressed a key to generate a new hotkey. /// private void Keyboard_OnKeyPressed(object? sender, Rampastring.XNAUI.Input.KeyPressEventArgs e) { foreach (var blacklistedKey in keyBlacklist) { if (e.PressedKey == blacklistedKey) return; } var currentModifiers = GetCurrentModifiers(); // The XNA keys seem to match the Windows virtual keycodes! This saves us some work pendingHotkey = new Hotkey(e.PressedKey, currentModifiers); lblCurrentlyAssignedTo.Text = string.Empty; foreach (var command in gameCommands) { if (pendingHotkey == command.Hotkey) lblCurrentlyAssignedTo.Text = "Currently assigned to:".L10N("Client:DTAConfig:CurrentAssignTo") + Environment.NewLine + command.UIName; } } private void BtnCancel_LeftClick(object? sender, EventArgs e) { Disable(); } private void BtnSave_LeftClick(object? sender, EventArgs e) { WriteKeyboardINI(); Disable(); } /// /// Updates the logic of the window. /// Used for keeping the "new hotkey" display in sync with the keyboard's /// modifier keys. /// /// Provides a snapshot of timing values. public override void Update(GameTime gameTime) { base.Update(gameTime); var oldModifiers = pendingHotkey.Modifier; var currentModifiers = GetCurrentModifiers(); if ((pendingHotkey.Key == Keys.None && currentModifiers != oldModifiers) || (pendingHotkey.Key != Keys.None && lastFrameModifiers == KeyModifiers.None && currentModifiers != lastFrameModifiers)) { pendingHotkey = new Hotkey(Keys.None, currentModifiers); lblCurrentlyAssignedTo.Text = string.Empty; } string displayString = pendingHotkey.ToString(); if (displayString != string.Empty) lblNewHotkeyValue.Text = pendingHotkey.ToString(); else lblNewHotkeyValue.Text = HOTKEY_TIP_TEXT; lastFrameModifiers = currentModifiers; } /// /// Detects which key modifiers (Ctrl, Shift, Alt) the user is currently pressing. /// private KeyModifiers GetCurrentModifiers() { var currentModifiers = KeyModifiers.None; if (Keyboard.IsKeyHeldDown(Keys.RightControl) || Keyboard.IsKeyHeldDown(Keys.LeftControl)) { currentModifiers |= KeyModifiers.Ctrl; } if (Keyboard.IsKeyHeldDown(Keys.RightShift) || Keyboard.IsKeyHeldDown(Keys.LeftShift)) { currentModifiers |= KeyModifiers.Shift; } if (Keyboard.IsKeyHeldDown(Keys.LeftAlt) || Keyboard.IsKeyHeldDown(Keys.RightAlt)) { currentModifiers |= KeyModifiers.Alt; } return currentModifiers; } private bool HasDuplicateHotkeys() { var assignedHotkeys = new HashSet(); foreach (var command in gameCommands) { if (command.Hotkey != null && command.Hotkey != Hotkey.None) { if (assignedHotkeys.Contains(command.Hotkey)) { #if DEBUG Debugger.Break(); #endif return true; } assignedHotkeys.Add(command.Hotkey); } } return false; } private void WriteKeyboardINI(bool writeEvenIfSettingsIniAsKeyboardIniHolds = false) { Debug.Assert(!HasDuplicateHotkeys(), "There are duplicate hotkeys assigned. How could this happen?"); IniFile keyboardIni = ClientConfiguration.Instance.SettingsIniAsKeyboardIni ? UserINISettings.Instance.SettingsIni : new IniFile() { FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.KeyboardINI) }; var hotkeySection = keyboardIni.GetOrAddSection(ClientConfiguration.Instance.KeyboardHotkeySection); foreach (var command in gameCommands) { // Note: we now explicitly differentiate between null and Hotkey.None if (command.Hotkey == null) { if (hotkeySection.KeyExists(command.ININame)) hotkeySection.RemoveKey(command.ININame); } else { hotkeySection.SetStringValue(command.ININame, command.Hotkey.GetTSEncoded().ToString()); } } // Do not write INI file if using Settings.ini as Keyboard.ini. The hot keys will be saved when Settings.ini is saved. // We choose this policy because, imagine a situation when the user pressed save in the hotkey config window, then decided they don't want changes (not the hotkey changes) they did in the options. // If we don't flush here, everything can be restored by hitting a cancel. // If we flush here -- the player can't cancel anymore at all. if (writeEvenIfSettingsIniAsKeyboardIniHolds || !ClientConfiguration.Instance.SettingsIniAsKeyboardIni) keyboardIni.WriteIniFile(); } /// /// A game command that can be assigned into a key on the keyboard. /// private class GameCommand { public GameCommand(string uiName, string category, string description, string iniName) { UIName = uiName; Category = category; Description = description; ININame = iniName; } /// /// Creates a game command and parses its information from an INI section. /// /// The INI section. public GameCommand(IniSection iniSection) { ININame = iniSection.SectionName; UIName = iniSection.GetStringValue("UIName", "Unnamed command") .L10N($"INI:Hotkeys:{ININame}:UIName"); string category = iniSection.GetStringValue("Category", "Unknown category"); Category = category.L10N($"INI:HotkeyCategories:{category}"); Description = iniSection.GetStringValue("Description", "Unknown description") .L10N($"INI:Hotkeys:{ININame}:Description"); int? defaultTSKey = iniSection.GetIntValueOrNull("DefaultKey"); DefaultHotkey = defaultTSKey.HasValue ? new Hotkey(defaultTSKey.Value) : null; // Note: currently, we treat Hotkey.None as null for default hotkeys, since it doesn't make much sense to have a default hotkey that is explicitly "no hotkey" -- Hotkey.None prevents automatically setting a new hot key via DefaultHotkey from a future update if (DefaultHotkey == Hotkey.None) DefaultHotkey = null; } public string UIName { get; private set; } public string Category { get; private set; } public string Description { get; private set; } public string ININame { get; private set; } public Hotkey? Hotkey { get; set; } public Hotkey? DefaultHotkey { get; private set; } } [Flags] private enum KeyModifiers { None = 0, Shift = 1, Ctrl = 2, Alt = 4 } /// /// Represents a keyboard key with modifiers. /// private sealed record Hotkey { public Keys Key { get; } public KeyModifiers Modifier { get; } public static readonly Hotkey None = new(Keys.None, KeyModifiers.None); /// /// Creates a new hotkey by decoding a Tiberian Sun / Red Alert 2 /// encoded key value. /// /// The encoded key value. public Hotkey(int encodedKeyValue) { Key = (Keys)(encodedKeyValue & 255); Modifier = (KeyModifiers)(encodedKeyValue >> 8); } public Hotkey(Keys key, KeyModifiers modifiers) { Key = key; Modifier = modifiers; } public override string ToString() { if (Key == Keys.None && Modifier == KeyModifiers.None) return string.Empty; return GetString(); } public string ToStringWithNone() { if (Key == Keys.None && Modifier == KeyModifiers.None) return "None".L10N("Client:DTAConfig:HotkeyNone"); return GetString(); } /// /// Creates the display string for this key. /// private string GetString() { string str = ""; if (Modifier.HasFlag(KeyModifiers.Shift)) str += "SHIFT+"; if (Modifier.HasFlag(KeyModifiers.Ctrl)) str += "CTRL+"; if (Modifier.HasFlag(KeyModifiers.Alt)) str += "ALT+"; if (Key == Keys.None) return str; return str + GetKeyDisplayString(Key); } /// /// Returns the hotkey in the Tiberian Sun / Red Alert 2 Keyboard.ini encoded format. /// public int GetTSEncoded() { return ((int)Modifier << 8) + (int)Key; } /// /// Returns the display string for an XNA key. /// Allows overriding specific key enum names to be more /// suitable for the UI. /// /// The key. /// A string. private string GetKeyDisplayString(Keys key) { switch (key) { case Keys.D0: return "0"; case Keys.D1: return "1"; case Keys.D2: return "2"; case Keys.D3: return "3"; case Keys.D4: return "4"; case Keys.D5: return "5"; case Keys.D6: return "6"; case Keys.D7: return "7"; case Keys.D8: return "8"; case Keys.D9: return "9"; case (Keys)12: return "NumPad5 (NumLock off)"; case (Keys)0x10: return "Shift"; case (Keys)0x11: return "Ctrl"; case (Keys)0x12: return "Alt"; default: return key.ToString(); } } } } } ================================================ FILE: ClientGUI/ICompositeControl.cs ================================================ using System.Collections.Generic; using Rampastring.XNAUI.XNAControls; namespace ClientGUI; /// /// Indicates that the implementer has sub-controls that need to be exposed to INI system. /// /// /// Currently only supported in . /// public interface ICompositeControl { /// /// The sub-controls that are exposed to the INI system. /// /// /// All the sub-controls should have their names set to something /// unique to each composite control. Utilise /// event to set the names of the sub-controls. /// IReadOnlyList SubControls { get; } } ================================================ FILE: ClientGUI/IME/DummyIMEHandler.cs ================================================ #nullable enable using Microsoft.Xna.Framework; namespace ClientGUI.IME { internal class DummyIMEHandler : IMEHandler { public DummyIMEHandler() { } public override bool TextCompositionEnabled { get => false; protected set { } } public override void SetTextInputRectangle(Rectangle rectangle) { } public override void StartTextComposition() { } public override void StopTextComposition() { } } } ================================================ FILE: ClientGUI/IME/IMEHandler.cs ================================================ #nullable enable using System; using System.Collections.Concurrent; using System.Diagnostics; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.Input; using Rampastring.XNAUI.XNAControls; namespace ClientGUI.IME; public abstract class IMEHandler : IIMEHandler { bool IIMEHandler.TextCompositionEnabled => TextCompositionEnabled; public abstract bool TextCompositionEnabled { get; protected set; } private XNATextBox? _IMEFocus = null; public XNATextBox? IMEFocus { get => _IMEFocus; protected set { _IMEFocus = value; Debug.Assert(!_IMEFocus?.IMEDisabled ?? true, "IME focus should not be assigned from a textbox with IME disabled"); } } private string _composition = string.Empty; public string Composition { get => _composition; protected set { string old = _composition; _composition = value; OnCompositionChanged(old, value); } } public bool CompositionEmpty => string.IsNullOrEmpty(_composition); /// /// Indicates whether an IME event has been received ever. Used to distinguish IME users from non-IME users. /// protected bool IMEEventReceived = false; protected bool LastActionIMEChatInput = true; private void OnCompositionChanged(string oldValue, string newValue) { //Debug.WriteLine($"IME: OnCompositionChanged: {newValue.Length - oldValue.Length}"); IMEEventReceived = true; // It seems that OnIMETextInput() is always triggered after OnCompositionChanged(). We expect such a behavior. LastActionIMEChatInput = false; } protected ConcurrentDictionary?> TextBoxHandleChatInputCallbacks = []; public virtual int CompositionCursorPosition { get; set; } public static IMEHandler Create(Game game) { #if DX return new WinFormsIMEHandler(game); #elif XNA // Warning: Think carefully before enabling WinFormsIMEHandler for XNA builds! // It *might* occasionally crash due to an unknown stack overflow issue. // This *might* be caused by both ImeSharp and XNAUI hooking into WndProc. // ImeSharp: https://github.com/ryancheung/ImeSharp/blob/dc2243beff9ef48eb37e398c506c905c965f8e68/ImeSharp/InputMethod.cs#L170 // XNAUI: https://github.com/Rampastring/Rampastring.XNAUI/blob/9a7d5bb3e47ea50286ee05073d0a6723bc6d764d/Input/KeyboardEventInput.cs#L79 // // That said, you can try returning a WinFormsIMEHandler and test if it is stable enough now. Who knows? return new DummyIMEHandler(); #elif GL return new SdlIMEHandler(game); #else #error Unknown variant #endif } public abstract void SetTextInputRectangle(Rectangle rectangle); public abstract void StartTextComposition(); public abstract void StopTextComposition(); protected virtual void OnIMETextInput(char character) { //Debug.WriteLine($"IME: OnIMETextInput: {character} {(short)character}; IMEFocus is null? {IMEFocus == null}"); LastActionIMEChatInput = true; if (IMEFocus != null) { TextBoxHandleChatInputCallbacks.TryGetValue(IMEFocus, out var handleChatInput); handleChatInput?.Invoke(character); } } public void SetIMETextInputRectangle(WindowManager manager) { // When the client window resizes, we should call SetIMETextInputRectangle() if (manager.SelectedControl is XNATextBox textBox) SetIMETextInputRectangle(textBox); } private void SetIMETextInputRectangle(XNATextBox sender) { WindowManager windowManager = sender.WindowManager; Rectangle textBoxRect = sender.RenderRectangle(); double scaleRatio = windowManager.ScaleRatio; Rectangle rect = new() { X = (int)(textBoxRect.X * scaleRatio + windowManager.SceneXPosition), Y = (int)(textBoxRect.Y * scaleRatio + windowManager.SceneYPosition), Width = (int)(textBoxRect.Width * scaleRatio), Height = (int)(textBoxRect.Height * scaleRatio) }; // The following code returns a more accurate location based on the current InputPosition. // However, as SetIMETextInputRectangle() does not automatically update with changes in InputPosition // (e.g., due to scrolling or mouse clicks altering the textbox's input position without shifting focus), // accuracy becomes inconsistent. Sometimes it's precise, other times it's off, // which is arguably worse than a consistent but manageable inaccuracy. // This inconsistency could lead to a confusing user experience, // as the input rectangle's position may not reliably reflect the current input position. // Therefore, unless whenever InputPosition is changed, SetIMETextInputRectangle() is raised // -- which requires more time to investigate and test, it's commented out for now. //var vec = Renderer.GetTextDimensions( // sender.Text.Substring(sender.TextStartPosition, sender.InputPosition), // sender.FontIndex); //rect.X += (int)(vec.X * scaleRatio); SetTextInputRectangle(rect); } void IIMEHandler.OnSelectedChanged(XNATextBox sender) { if (sender.WindowManager.SelectedControl == sender) { StopTextComposition(); if (!sender.IMEDisabled && sender.Enabled && sender.Visible) { IMEFocus = sender; // Update the location of IME based on the textbox SetIMETextInputRectangle(sender); StartTextComposition(); } else { IMEFocus = null; } } else if (sender.WindowManager.SelectedControl is not XNATextBox) { // Disable IME since the current selected control is not XNATextBox IMEFocus = null; StopTextComposition(); } // Note: if sender.WindowManager.SelectedControl != sender and is XNATextBox, // another OnSelectedChanged() will be triggered, // so we do not need to handle this case } void IIMEHandler.RegisterXNATextBox(XNATextBox sender, Action? handleCharInput) => TextBoxHandleChatInputCallbacks[sender] = handleCharInput; void IIMEHandler.KillXNATextBox(XNATextBox sender) => TextBoxHandleChatInputCallbacks.TryRemove(sender, out _); bool IIMEHandler.HandleScrollLeftKey(XNATextBox sender) => !CompositionEmpty; bool IIMEHandler.HandleScrollRightKey(XNATextBox sender) => !CompositionEmpty; bool IIMEHandler.HandleBackspaceKey(XNATextBox sender) { bool handled = !LastActionIMEChatInput; LastActionIMEChatInput = true; //Debug.WriteLine($"IME: HandleBackspaceKey: handled: {handled}"); return handled; } bool IIMEHandler.HandleDeleteKey(XNATextBox sender) { bool handled = !LastActionIMEChatInput; LastActionIMEChatInput = true; //Debug.WriteLine($"IME: HandleDeleteKey: handled: {handled}"); return handled; } bool IIMEHandler.GetDrawCompositionText(XNATextBox sender, out string composition, out int compositionCursorPosition) { if (IMEFocus != sender || CompositionEmpty) { composition = string.Empty; compositionCursorPosition = 0; return false; } composition = Composition; compositionCursorPosition = CompositionCursorPosition; return true; } bool IIMEHandler.HandleCharInput(XNATextBox sender, char input) => TextCompositionEnabled; bool IIMEHandler.HandleEnterKey(XNATextBox sender) => false; bool IIMEHandler.HandleEscapeKey(XNATextBox sender) { //Debug.WriteLine($"IME: HandleEscapeKey: handled: {IMEEventReceived}"); // This method disables the ESC handling of the TextBox as long as the user has ever used IME. // This is because IME users often use ESC to cancel composition. Even if currently the composition is empty, // the user still expects ESC to cancel composition rather than deleting the whole sentence. // For example, the user might mistakenly hit ESC key twice to cancel composition -- deleting the whole sentence is definitely a heavy punishment for such a small mistake. // Note: "!CompositionEmpty => IMEEventReceived" should hold, but just in case return IMEEventReceived || !CompositionEmpty; } void IIMEHandler.OnTextChanged(XNATextBox sender) { } } ================================================ FILE: ClientGUI/IME/SdlIMEHandler.cs ================================================ #nullable enable using Microsoft.Xna.Framework; namespace ClientGUI.IME; /// /// Integrate IME to DesktopGL(SDL2) platform. /// /// /// Note: We were unable to provide reliable input method support for /// SDL2 due to the lack of a way to be able to stabilize hooks for /// the SDL2 main loop.
/// Perhaps this requires some changes in Monogame. ///
internal sealed class SdlIMEHandler(Game game) : DummyIMEHandler { } ================================================ FILE: ClientGUI/IME/WinFormsIMEHandler.cs ================================================ #nullable enable using System; using ImeSharp; using Microsoft.Xna.Framework; using Rampastring.Tools; namespace ClientGUI.IME; /// /// Integrate IME to XNA framework. /// internal class WinFormsIMEHandler : IMEHandler { public override bool TextCompositionEnabled { get => InputMethod.Enabled; protected set { if (value != InputMethod.Enabled) InputMethod.Enabled = value; } } public WinFormsIMEHandler(Game game) { Logger.Log($"Initialize WinFormsIMEHandler."); if (game?.Window?.Handle == null) throw new Exception("The handle of game window should not be null"); InputMethod.Initialize(game.Window.Handle); InputMethod.TextInputCallback = OnIMETextInput; InputMethod.TextCompositionCallback = (compositionText, cursorPosition) => { Composition = compositionText.ToString(); CompositionCursorPosition = cursorPosition; }; } public override void StartTextComposition() { //Debug.WriteLine("IME: StartTextComposition"); TextCompositionEnabled = true; } public override void StopTextComposition() { //Debug.WriteLine("IME: StopTextComposition"); TextCompositionEnabled = false; } public override void SetTextInputRectangle(Rectangle rect) => InputMethod.SetTextInputRect(rect.X, rect.Y, rect.Width, rect.Height); } ================================================ FILE: ClientGUI/INIConfigException.cs ================================================ using System; namespace ClientGUI { /// /// The exception that is thrown when INI data is invalid. /// public class INIConfigException : Exception { public INIConfigException(string message) : base(message) { } } } ================================================ FILE: ClientGUI/INItializableWindow.cs ================================================ using ClientCore; using ClientCore.I18N; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace ClientGUI { public class INItializableWindow : XNAPanel { public INItializableWindow(WindowManager windowManager) : base(windowManager) { } protected CCIniFile ConfigIni { get; private set; } private bool hasCloseButton = false; private bool _initialized = false; /// /// If not null, the client will read an INI file with this name /// instead of the window's name. /// protected string IniNameOverride { get; set; } private static bool AnyChildMatches(IEnumerable list, Func isTargetControl) { foreach (XNAControl child in list) { bool matched = isTargetControl(child); if (matched) return true; matched = AnyChildMatches(child.Children, isTargetControl); if (matched) return true; } return false; } public T FindChild(string childName, bool optional = false) where T : XNAControl { XNAControl result = null; AnyChildMatches(new List() { this }, control => { if (control.Name != childName) return false; result = control; return true; }); if (result == null && !optional) throw new KeyNotFoundException("Could not find required child control: " + childName); return (T)result; } public List FindChildrenStartWith(string prefix) where T : XNAControl { List result = new List(); AnyChildMatches(new List() { this }, control => { if (string.IsNullOrEmpty(prefix) || !string.IsNullOrEmpty(control.Name) && control.Name.StartsWith(prefix)) result.Add((T)control); return false; }); return result; } /// /// Attempts to locate the ini config file for the current control. /// Only return a config path if it exists. /// /// The ini config file path protected string GetConfigPath() { string iniFileName = string.IsNullOrWhiteSpace(IniNameOverride) ? Name : IniNameOverride; // get theme specific path FileInfo configIniPath = SafePath.GetFile(ProgramConstants.GetResourcePath(), FormattableString.Invariant($"{iniFileName}.ini")); if (configIniPath.Exists) return configIniPath.FullName; // get base path configIniPath = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($"{iniFileName}.ini")); if (configIniPath.Exists) return configIniPath.FullName; if (iniFileName == Name) return null; // IniNameOverride must be null, no need to continue iniFileName = Name; // get theme specific path configIniPath = SafePath.GetFile(ProgramConstants.GetResourcePath(), FormattableString.Invariant($"{iniFileName}.ini")); if (configIniPath.Exists) return configIniPath.FullName; // get base path configIniPath = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($"{iniFileName}.ini")); return configIniPath.Exists ? configIniPath.FullName : null; } public override void Initialize() { if (_initialized) throw new InvalidOperationException("INItializableWindow cannot be initialized twice."); string configIniPath = GetConfigPath(); if (string.IsNullOrEmpty(configIniPath)) { base.Initialize(); return; } ConfigIni = new CCIniFile(configIniPath); if (Parser.Instance == null) _ = new Parser(WindowManager); // Note: Parser.Instance will be set by calling new Parser() Parser.Instance.SetPrimaryControl(this); ReadINIForControl(this); ReadLateAttributesForControl(this); ParseExtraControls(); base.Initialize(); _initialized = true; } private void ParseExtraControls() { var section = ConfigIni.GetSection("$ExtraControls"); if (section == null) return; foreach (var kvp in section.Keys) { if (!kvp.Key.StartsWith("$CC")) continue; string[] parts = kvp.Value.Split(':'); if (parts.Length != 2) throw new ClientConfigurationException("Invalid $ExtraControl specified in " + Name + ": " + kvp.Value); if (!Children.Any(child => child.Name == parts[0])) { var control = CreateChildControl(this, kvp.Value); control.Name = parts[0]; control.DrawOrder = -Children.Count; ReadINIForControl(control); } } } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "HasCloseButton") hasCloseButton = iniFile.GetBooleanValue(Name, key, hasCloseButton); base.ParseControlINIAttribute(iniFile, key, value); } protected void ReadINIForControl(XNAControl control) { var section = ConfigIni.GetSection(control.Name); if (section == null) return; Parser.Instance.SetPrimaryControl(this); // shorthand for localization function static string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true) => Translation.Instance.LookUp(control, attributeName, defaultValue, notify); foreach (var kvp in section.Keys) { if (kvp.Key.StartsWith("$CC")) { var child = CreateChildControl(control, kvp.Value); ReadINIForControl(child); child.Initialize(); if (child is ICompositeControl composite) { foreach (var sc in composite.SubControls) { ReadINIForControl(sc); sc.Initialize(); } } } else if (kvp.Key == "$X") { control.X = Parser.Instance.GetExprValue( Localize(control, kvp.Key, kvp.Value, notify: false), control); } else if (kvp.Key == "$Y") { control.Y = Parser.Instance.GetExprValue( Localize(control, kvp.Key, kvp.Value, notify: false), control); } else if (kvp.Key == "$Width") { control.Width = Parser.Instance.GetExprValue( Localize(control, kvp.Key, kvp.Value, notify: false), control); } else if (kvp.Key == "$Height") { control.Height = Parser.Instance.GetExprValue( Localize(control, kvp.Key, kvp.Value, notify: false), control); } else if (kvp.Key == "$TextAnchor" && control is XNALabel) { // TODO refactor these to be more object-oriented ((XNALabel)control).TextAnchor = (LabelTextAnchorInfo)Enum.Parse(typeof(LabelTextAnchorInfo), kvp.Value); } else if (kvp.Key == "$AnchorPoint" && control is XNALabel) { string[] parts = kvp.Value.Split(','); if (parts.Length != 2) throw new FormatException("Invalid format for AnchorPoint: " + kvp.Value); ((XNALabel)control).AnchorPoint = new Vector2(Parser.Instance.GetExprValue(parts[0], control), Parser.Instance.GetExprValue(parts[1], control)); } else if (kvp.Key == "$LeftClickAction") { if (kvp.Value == "Disable") control.LeftClick += (s, e) => Disable(); } else { control.ParseINIAttribute(ConfigIni, kvp.Key, kvp.Value); } } } /// /// Reads a second set of attributes for a control's child controls. /// Enables linking controls to controls that are defined after them. /// private void ReadLateAttributesForControl(XNAControl control) { var section = ConfigIni.GetSection(control.Name); if (section == null) return; var children = Children.ToList(); foreach (var child in children) { // This logic should also be enabled for other types in the future, // but it requires changes in XNAUI if (!(child is XNATextBox)) continue; var childSection = ConfigIni.GetSection(child.Name); if (childSection == null) continue; string nextControl = childSection.GetStringValue("NextControl", null); if (!string.IsNullOrWhiteSpace(nextControl)) { var otherChild = children.Find(c => c.Name == nextControl); if (otherChild != null) ((XNATextBox)child).NextControl = otherChild; } string previousControl = childSection.GetStringValue("PreviousControl", null); if (!string.IsNullOrWhiteSpace(previousControl)) { var otherChild = children.Find(c => c.Name == previousControl); if (otherChild != null) ((XNATextBox)child).PreviousControl = otherChild; } } } private XNAControl CreateChildControl(XNAControl parent, string keyValue) { string[] parts = keyValue.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) throw new INIConfigException("Invalid child control definition " + keyValue); string childName = parts[0]; if (string.IsNullOrEmpty(childName)) throw new INIConfigException("Empty name in child control definition for " + parent.Name); XNAControl childControl = ClientGUICreator.GetXnaControl(parts[1]); if (Array.Exists(childName.ToCharArray(), c => !char.IsLetterOrDigit(c) && c != '_')) throw new INIConfigException("Names of INItializableWindow child controls must consist of letters, digits and underscores only. Offending name: " + parts[0]); childControl.Name = childName; parent.AddChildWithoutInitialize(childControl); return childControl; } } } ================================================ FILE: ClientGUI/IToolTipContainer.cs ================================================ namespace ClientGUI; public interface IToolTipContainer { public ToolTip ToolTip { get; } public string ToolTipText { get; set; } } ================================================ FILE: ClientGUI/Parser.cs ================================================ /********************************************************************* * Dawn of the Tiberium Age MonoGame/XNA CnCNet Client * Expression Parser * Copyright (C) Rampastring 2022 * * The CnCNet Client is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The CnCNet Client is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program.If not, see. * *********************************************************************/ using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; namespace ClientGUI { /// /// Parses arithmetic expressions. /// class Parser { private const int CHAR_VALUE_ZERO = 48; public Parser(WindowManager windowManager) { if (_instance != null) throw new InvalidOperationException("Only one instance of Parser can exist at a time."); globalConstants = new Dictionary(); globalConstants.Add("RESOLUTION_WIDTH", windowManager.RenderResolutionX); globalConstants.Add("RESOLUTION_HEIGHT", windowManager.RenderResolutionY); IniSection parserConstantsSection = ClientConfiguration.Instance.GetParserConstants(); if (parserConstantsSection != null) { foreach (var kvp in parserConstantsSection.Keys) globalConstants.Add(kvp.Key, Conversions.IntFromString(kvp.Value, 0)); } _instance = this; } private static Parser _instance; public static Parser Instance => _instance; private static Dictionary globalConstants; public string Input { get; private set; } private int tokenPlace; private XNAControl primaryControl; private XNAControl parsingControl; private XNAControl GetControl(string controlName) { if (controlName == primaryControl.Name) return primaryControl; var control = Find(primaryControl.Children, controlName); if (control == null) throw new KeyNotFoundException($"Control '{controlName}' not found while parsing input '{Input}'"); return control; } private XNAControl Find(IEnumerable list, string controlName) { foreach (XNAControl child in list) { if (child.Name == controlName) return child; XNAControl childOfChild = Find(child.Children, controlName); if (childOfChild != null) return childOfChild; } return null; } private int GetConstant(string constantName) { if (!globalConstants.TryGetValue(constantName, out int value)) throw new KeyNotFoundException($"Constant '{constantName}' not found. " + $"Please check [ParserConstants] section in either {ClientConfiguration.CLIENT_SETTINGS} file, " + $"or any possible files that {ClientConfiguration.CLIENT_SETTINGS} depends on, e.g., GlobalThemeSettings.ini.'"); return value; } public void SetPrimaryControl(XNAControl primaryControl) { this.primaryControl = primaryControl; } public int GetExprValue(string input, XNAControl parsingControl) { this.parsingControl = parsingControl; Input = input; tokenPlace = 0; return GetExprValue(); } private int GetExprValue() { int value = 0; while (true) { SkipWhitespace(); if (IsEndOfInput()) return value; char c = Input[tokenPlace]; if (char.IsDigit(c)) { value = GetInt(); } else if (c == '+') { tokenPlace++; value += GetNumericalValue(); } else if (c == '-') { tokenPlace++; value -= GetNumericalValue(); } else if (c == '/') { tokenPlace++; value /= GetExprValue(); } else if (c == '*') { tokenPlace++; value *= GetExprValue(); } else if (c == '(') { tokenPlace++; value = GetExprValue(); } else if (c == ')') { tokenPlace++; return value; } else if (char.IsUpper(c)) { value = GetConstantValue(); } else if (char.IsLower(c)) { value = GetFunctionValue(); } } } private int GetNumericalValue() { SkipWhitespace(); if (IsEndOfInput()) return 0; char c = Input[tokenPlace]; if (char.IsDigit(c)) { return GetInt(); } else if (char.IsUpper(c)) { return GetConstantValue(); } else if (char.IsLower(c)) { return GetFunctionValue(); } else if (c == '(') { tokenPlace++; return GetExprValue(); } else { throw new INIConfigException("Unexpected character " + c + " when parsing input: " + Input); } } private void SkipWhitespace() { while (true) { if (IsEndOfInput()) return; char c = Input[tokenPlace]; if (c == ' ' || c == '\r' || c == '\n') tokenPlace++; else break; } } private string GetIdentifier() { string identifierName = ""; while (true) { if (IsEndOfInput()) break; char c = Input[tokenPlace]; if (char.IsWhiteSpace(c)) break; if (!char.IsLetterOrDigit(c) && c != '_' && c != '$' && c != '.') break; identifierName += c.ToString(); tokenPlace++; } return identifierName; } private int GetConstantValue() { string constantName = GetIdentifier(); return GetConstant(constantName); } private int GetFunctionValue() { string functionName = GetIdentifier(); SkipWhitespace(); ConsumeChar('('); string paramName = GetIdentifier(); SkipWhitespace(); ConsumeChar(')'); if (paramName == "$ParentControl") { if (parsingControl.Parent == null) throw new INIConfigException("$ParentControl used for control that has no parent: " + parsingControl.Name); paramName = parsingControl.Parent.Name; } else if (paramName == "$Self") { paramName = parsingControl.Name; } switch (functionName) { case "getX": return GetControl(paramName).X; case "getY": return GetControl(paramName).Y; case "getWidth": return GetControl(paramName).Width; case "getHeight": return GetControl(paramName).Height; case "getBottom": return GetControl(paramName).Bottom; case "getRight": return GetControl(paramName).Right; case "horizontalCenterOnParent": parsingControl.CenterOnParentHorizontally(); return parsingControl.X; default: throw new INIConfigException("Unknown function " + functionName + " in expression " + Input); } } private void ConsumeChar(char token) { if (Input[tokenPlace] != token) throw new INIConfigException($"Parse error: expected '{token}' in expression {Input}. Instead encountered '{Input[tokenPlace]}'."); tokenPlace++; } private int GetInt() { int value = 0; while (true) { if (IsEndOfInput()) return value; char c = Input[tokenPlace]; if (!char.IsDigit(c)) return value; value = (value * 10) + Input[tokenPlace] - CHAR_VALUE_ZERO; tokenPlace++; } } private bool IsEndOfInput() => tokenPlace >= Input.Length; } } ================================================ FILE: ClientGUI/ScreenResolution.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Linq; using ClientCore; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ClientGUI { /// /// A single screen resolution. /// public sealed record ScreenResolution : IComparable { /// /// The width of the resolution in pixels. /// public int Width { get; } /// /// The height of the resolution in pixels. /// public int Height { get; } public ScreenResolution(int width, int height) { Width = width; Height = height; } public ScreenResolution(Rectangle rectangle) { Width = rectangle.Width; Height = rectangle.Height; } public ScreenResolution(string resolution) { List resolutionList = resolution.Trim().Split('x').Take(2).Select(int.Parse).ToList(); Width = resolutionList[0]; Height = resolutionList[1]; } public static implicit operator ScreenResolution(string resolution) => new(resolution); public sealed override string ToString() => Width + "x" + Height; public static implicit operator string(ScreenResolution resolution) => resolution.ToString(); public void Deconstruct(out int width, out int height) { width = this.Width; height = this.Height; } public static implicit operator ScreenResolution((int Width, int Height) resolutionTuple) => new(resolutionTuple.Width, resolutionTuple.Height); public static implicit operator (int Width, int Height)(ScreenResolution resolution) => new(resolution.Width, resolution.Height); public bool Fits(ScreenResolution child) => this.Width >= child.Width && this.Height >= child.Height; public int CompareTo(ScreenResolution? other) { if (other is null) return 1; return (this.Width, this.Height).CompareTo((other.Width, other.Height)); } // Accessing GraphicsAdapter.DefaultAdapter requiring DXMainClient.GameClass has been constructed. Lazy loading prevents possible null reference issues for now. private static ScreenResolution? _desktopResolution = null; /// /// The resolution of primary monitor. /// public static ScreenResolution DesktopResolution => _desktopResolution ??= new(GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height); // The default graphic profile supports resolution up to 4096x4096. The number gets even smaller in practice. Therefore, we select 3840 as the limit. public static ScreenResolution HiDefLimitResolution { get; } = "3840x3840"; private static ScreenResolution? _safeMaximumResolution = null; /// /// The resolution of primary monitor, or the maximum resolution supported by the graphic profile, whichever is smaller. /// public static ScreenResolution SafeMaximumResolution { get { #if XNA return _safeMaximumResolution ??= HiDefLimitResolution.Fits(DesktopResolution) ? DesktopResolution : HiDefLimitResolution; #else return _safeMaximumResolution ??= DesktopResolution; #endif } } private static ScreenResolution? _safeFullScreenResolution = null; /// /// The maximum resolution supported by the graphic profile, or the largest full screen resolution supported by the primary monitor, whichever is smaller. /// public static ScreenResolution SafeFullScreenResolution => _safeFullScreenResolution ??= GetFullScreenResolutions(minWidth: 800, minHeight: 600).Max ?? SafeMaximumResolution; public static SortedSet GetFullScreenResolutions(int minWidth, int minHeight) => GetFullScreenResolutions(minWidth, minHeight, SafeMaximumResolution.Width, SafeMaximumResolution.Height); public static SortedSet GetFullScreenResolutions(int minWidth, int minHeight, int maxWidth, int maxHeight) { SortedSet screenResolutions = []; foreach (DisplayMode dm in GraphicsAdapter.DefaultAdapter.SupportedDisplayModes) { if (dm.Width < minWidth || dm.Height < minHeight || dm.Width > maxWidth || dm.Height > maxHeight) continue; var resolution = new ScreenResolution(dm.Width, dm.Height); // SupportedDisplayModes can include the same resolution multiple times // because it takes the refresh rate into consideration. // Which will be filtered out by HashSet screenResolutions.Add(resolution); } return screenResolutions; } public static readonly IReadOnlyList OptimalWindowedResolutions = [ "1024x600", "1024x720", "1280x600", "1280x720", "1280x768", "1280x800", ]; public const int MAX_INT_SCALE = 9; public SortedSet GetIntegerScaledResolutions() => GetIntegerScaledResolutions(SafeMaximumResolution); public SortedSet GetIntegerScaledResolutions(ScreenResolution maxResolution) { SortedSet resolutions = []; for (int i = 1; i <= MAX_INT_SCALE; i++) { ScreenResolution scaledResolution = (this.Width * i, this.Height * i); if (maxResolution.Fits(scaledResolution)) resolutions.Add(scaledResolution); else break; } return resolutions; } public static SortedSet GetWindowedResolutions(int minWidth, int minHeight) => GetWindowedResolutions(minWidth, minHeight, SafeMaximumResolution.Width, SafeMaximumResolution.Height); public static SortedSet GetWindowedResolutions(IEnumerable optimalResolutions, int minWidth, int minHeight) => GetWindowedResolutions(OptimalWindowedResolutions, minWidth, minHeight, SafeMaximumResolution.Width, SafeMaximumResolution.Height); public static SortedSet GetWindowedResolutions(int minWidth, int minHeight, int maxWidth, int maxHeight) => GetWindowedResolutions(OptimalWindowedResolutions, minWidth, minHeight, maxWidth, maxHeight); public static SortedSet GetWindowedResolutions(IEnumerable optimalResolutions, int minWidth, int minHeight, int maxWidth, int maxHeight) { ScreenResolution maxResolution = (maxWidth, maxHeight); SortedSet windowedResolutions = []; foreach (ScreenResolution optimalResolution in optimalResolutions) { if (optimalResolution.Width < minWidth || optimalResolution.Height < minHeight) continue; if (!maxResolution.Fits(optimalResolution)) continue; windowedResolutions.Add(optimalResolution); } return windowedResolutions; } public static SortedSet GetRecommendedResolutions() { List recommendedResolutions = ClientConfiguration.Instance.RecommendedResolutions.Select(resolution => (ScreenResolution)resolution).ToList(); SortedSet scaledRecommendedResolutions = [.. recommendedResolutions.SelectMany(resolution => resolution.GetIntegerScaledResolutions())]; return scaledRecommendedResolutions; } public static ScreenResolution GetBestRecommendedResolution() => GetRecommendedResolutions().Max ?? SafeFullScreenResolution; } } ================================================ FILE: ClientGUI/Settings/FileSettingCheckBox.cs ================================================ using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; using System.Collections.Generic; using System.IO; using System.Linq; namespace ClientGUI.Settings { /// /// A check-box that toggles between two sets of files and saves the setting to user settings file. /// public class FileSettingCheckBox : SettingCheckBoxBase, IFileSetting { public FileSettingCheckBox(WindowManager windowManager) : base(windowManager) { } public FileSettingCheckBox(WindowManager windowManager, bool defaultValue, string settingSection, string settingKey, bool checkAvailability = false, bool resetUnavailableValue = false, bool restartRequired = false) : base(windowManager, defaultValue, settingSection, settingKey, restartRequired) { CheckAvailability = checkAvailability; ResetUnavailableValue = resetUnavailableValue; } public bool CheckAvailability { get; set; } public bool ResetUnavailableValue { get; set; } private List enabledFiles = new List(); private List disabledFiles = new List(); private bool EnabledFilesComplete => enabledFiles.All(f => File.Exists(f.SourcePath)); private bool DisabledFilesComplete => disabledFiles.All(f => File.Exists(f.SourcePath)); // Backwards compatibility with old FileSettingCheckBox implementation. private bool useLegacyImplementation = false; private bool reversed = false; public override void GetAttributes(IniFile iniFile) { base.GetAttributes(iniFile); var section = iniFile.GetSection(Name); if (section == null) return; var files = FileSourceDestinationInfo.ParseFSDInfoList(section, "File"); if (files.Count > 0) { enabledFiles = files; useLegacyImplementation = true; } else { enabledFiles = FileSourceDestinationInfo.ParseFSDInfoList(section, "EnabledFile"); disabledFiles = FileSourceDestinationInfo.ParseFSDInfoList(section, "DisabledFile"); } } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "CheckAvailability": CheckAvailability = Conversions.BooleanFromString(value, false); return; case "ResetUnavailableValue": ResetUnavailableValue = Conversions.BooleanFromString(value, false); return; case "Reversed": reversed = Conversions.BooleanFromString(value, false); return; } base.ParseControlINIAttribute(iniFile, key, value); } public bool RefreshSetting() { if (useLegacyImplementation) return false; bool currentValue = Checked; if (CheckAvailability) { Enabled = true; if (ResetUnavailableValue) { if (DisabledFilesComplete != EnabledFilesComplete) Checked = EnabledFilesComplete; else if (!DisabledFilesComplete && !EnabledFilesComplete) Checked = DefaultValue; } } return Checked != currentValue; } public void AddEnabledFile(string source, string destination, FileOperationOption option) => enabledFiles.Add(new FileSourceDestinationInfo(source, destination, option)); public void AddDisabledFile(string source, string destination, FileOperationOption option) => disabledFiles.Add(new FileSourceDestinationInfo(source, destination, option)); public override void Load() { if (useLegacyImplementation) Checked = reversed != File.Exists(enabledFiles[0].DestinationPath); else Checked = UserINISettings.Instance.GetValue(SettingSection, SettingKey, DefaultValue); originalState = Checked; } public override bool Save() { if (useLegacyImplementation) { if (reversed != Checked) enabledFiles.ForEach(f => f.Apply()); else enabledFiles.ForEach(f => f.Revert()); return RestartRequired && (Checked != originalState); } bool canBeChecked = !CheckAvailability || EnabledFilesComplete; bool canBeUnchecked = !CheckAvailability || DisabledFilesComplete; if (Checked && canBeChecked) { disabledFiles.ForEach(f => f.Revert()); enabledFiles.ForEach(f => f.Apply()); } else if (!Checked && canBeUnchecked) { enabledFiles.ForEach(f => f.Revert()); disabledFiles.ForEach(f => f.Apply()); } else // selected state is unavailable, don't do anything { Logger.Log($"{nameof(FileSettingCheckBox)}: " + $"The selected state ({Checked}) is unavailable in {Name}"); return false; } UserINISettings.Instance.SetValue(SettingSection, SettingKey, Checked); return RestartRequired && (Checked != originalState); } } } ================================================ FILE: ClientGUI/Settings/FileSettingDropDown.cs ================================================ using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; using System.Collections.Generic; using System.IO; namespace ClientGUI.Settings { /// /// A dropdown that switches between multiple sets of files. /// public class FileSettingDropDown : SettingDropDownBase, IFileSetting { public FileSettingDropDown(WindowManager windowManager) : base(windowManager) { } public FileSettingDropDown(WindowManager windowManager, int defaultValue, string settingSection, string settingKey, bool checkAvailability = false, bool resetUnavailableValue = false, bool restartRequired = false) : base(windowManager, defaultValue, settingSection, settingKey, restartRequired) { CheckAvailability = checkAvailability; ResetUnavailableValue = resetUnavailableValue; } private readonly List> itemFilesList = new List>(); public bool CheckAvailability { get; private set; } public bool ResetUnavailableValue { get; private set; } public override void GetAttributes(IniFile iniFile) { base.GetAttributes(iniFile); var section = iniFile.GetSection(Name); if (section == null) return; for (int i = 0; i < Items.Count; i++) itemFilesList.Add(FileSourceDestinationInfo.ParseFSDInfoList(section, $"Item{i}File")); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "CheckAvailability": CheckAvailability = Conversions.BooleanFromString(value, false); return; case "ResetUnavailableValue": ResetUnavailableValue = Conversions.BooleanFromString(value, false); return; } base.ParseControlINIAttribute(iniFile, key, value); } public bool RefreshSetting() { int currentValue = SelectedIndex; if (CheckAvailability) { for (int i = 0; i < Items.Count; i++) { Items[i].Selectable = true; foreach (var fileInfo in itemFilesList[i]) { if (!File.Exists(fileInfo.SourcePath)) { Items[i].Selectable = false; break; } } } if (ResetUnavailableValue && !Items[SelectedIndex].Selectable) SelectedIndex = DefaultValue; } return SelectedIndex != currentValue; } public void AddFile(int itemIndex, string source, string destination, FileOperationOption option) { if (itemIndex < 0 || itemIndex >= Items.Count) return; if (itemFilesList.Count < itemIndex + 1) itemFilesList.Add(new List()); itemFilesList[itemIndex].Add(new FileSourceDestinationInfo(source, destination, option)); } public override void Load() { SelectedIndex = UserINISettings.Instance.GetValue(SettingSection, SettingKey, DefaultValue); originalState = SelectedIndex; } public override bool Save() { if (Items[SelectedIndex].Selectable) { for (int i = 0; i < itemFilesList.Count; i++) { if (i != SelectedIndex) itemFilesList[i].ForEach(f => f.Revert()); } itemFilesList[SelectedIndex].ForEach(f => f.Apply()); } else // selected item is unavailable, don't do anything { Logger.Log($"{nameof(FileSettingDropDown)}: " + $"The selected item \"{Items[SelectedIndex].Text}\" ({Items[SelectedIndex].Tag}) is unavailable in {Name}."); return false; } UserINISettings.Instance.SetValue(SettingSection, SettingKey, SelectedIndex); return RestartRequired && (SelectedIndex != originalState); } } } ================================================ FILE: ClientGUI/Settings/FileSourceDestinationInfo.cs ================================================ using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; using System; using System.Collections.Generic; using System.IO; namespace ClientGUI.Settings { sealed class FileSourceDestinationInfo { private readonly string destinationPath; private readonly string sourcePath; public string SourcePath => SafePath.CombineFilePath(ProgramConstants.GamePath, sourcePath); public string DestinationPath => SafePath.CombineFilePath(ProgramConstants.GamePath, destinationPath); /// /// A path where the files edited by user are saved if /// is set to . /// public string CachedPath => SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "SettingsCache", sourcePath); public FileOperationOption FileOperationOption { get; } public FileSourceDestinationInfo(string source, string destination, FileOperationOption option) { sourcePath = source; destinationPath = destination; FileOperationOption = option; } /// /// Constructs a new instance of from a given string. /// /// A string to be parsed. public FileSourceDestinationInfo(string value) { string[] parts = value.Split(','); if (parts.Length < 2) throw new ArgumentException($"{nameof(FileSourceDestinationInfo)}: " + $"Too few parameters specified in parsed value", nameof(value)); FileOperationOption option = default(FileOperationOption); if (parts.Length >= 3) { bool success = Enum.TryParse(parts[2], out option); if (!success) throw new ArgumentException($"{nameof(FileSourceDestinationInfo)}: " + $"Error parsing FileOperationOption enum", nameof(value)); } sourcePath = parts[0]; destinationPath = parts[1]; FileOperationOption = option; } /// /// A method which parses certain key list values from an INI section /// into a list of objects. /// /// An INI section to parse key values from. /// A string to append index to when /// parsing the values from key list. /// A of all correctly defined s. public static List ParseFSDInfoList(IniSection section, string iniKeyPrefix) { if (section == null) throw new ArgumentNullException(nameof(section)); List result = new List(); string fileInfo; for (int i = 0; !string.IsNullOrWhiteSpace( fileInfo = section.GetStringValue($"{iniKeyPrefix}{i}", string.Empty)); i++) { result.Add(new FileSourceDestinationInfo(fileInfo)); } return result; } /// /// Performs file operations from to /// according to . /// public void Apply() { switch (FileOperationOption) { case FileOperationOption.OverwriteOnMismatch: string sourceHash = Utilities.CalculateSHA1ForFile(SourcePath); string destinationHash = Utilities.CalculateSHA1ForFile(DestinationPath); if (sourceHash != destinationHash) File.Copy(SourcePath, DestinationPath, true); break; case FileOperationOption.DontOverwrite: if (!File.Exists(DestinationPath)) File.Copy(SourcePath, DestinationPath, false); break; case FileOperationOption.KeepChanges: if (!File.Exists(DestinationPath)) { if (File.Exists(CachedPath)) File.Move(CachedPath, DestinationPath); else File.Copy(SourcePath, DestinationPath, true); } break; case FileOperationOption.AlwaysOverwrite: File.Copy(SourcePath, DestinationPath, true); break; case FileOperationOption.AlwaysOverwrite_LinkAsReadOnly: FileExtensions.CreateHardLinkFromSource(sourcePath, destinationPath, fallback: true); new FileInfo(DestinationPath).IsReadOnly = true; new FileInfo(SourcePath).IsReadOnly = true; break; default: throw new InvalidOperationException($"{nameof(FileSourceDestinationInfo)}: " + $"Invalid {nameof(FileOperationOption)} value of {FileOperationOption}"); } } /// /// Performs file operations to undo changes made by /// to according to . /// public void Revert() { switch (FileOperationOption) { case FileOperationOption.KeepChanges: if (File.Exists(DestinationPath)) { if (!File.Exists(Path.GetDirectoryName(CachedPath))) SafePath.GetDirectory(Path.GetDirectoryName(CachedPath)).Create(); File.Move(DestinationPath, CachedPath); } break; case FileOperationOption.AlwaysOverwrite_LinkAsReadOnly: case FileOperationOption.OverwriteOnMismatch: case FileOperationOption.DontOverwrite: case FileOperationOption.AlwaysOverwrite: if (File.Exists(DestinationPath)) { FileInfo destinationFile = new(DestinationPath); destinationFile.IsReadOnly = false; destinationFile.Delete(); } if (FileOperationOption == FileOperationOption.AlwaysOverwrite_LinkAsReadOnly) new FileInfo(SourcePath).IsReadOnly = false; break; default: throw new InvalidOperationException($"{nameof(FileSourceDestinationInfo)}: " + $"Invalid {nameof(FileOperationOption)} value of {FileOperationOption}"); } } } /// /// Defines the expected behavior of file operations performed with /// . /// public enum FileOperationOption { AlwaysOverwrite = 0, OverwriteOnMismatch, DontOverwrite, KeepChanges, AlwaysOverwrite_LinkAsReadOnly, } } ================================================ FILE: ClientGUI/Settings/IFileSetting.cs ================================================ namespace ClientGUI.Settings { interface IFileSetting : IUserSetting { /// /// Determines if the setting availability is checked on runtime. /// bool CheckAvailability { get; } /// /// Determines if the client would adjust the setting value automatically /// if the current value becomes unavailable. /// bool ResetUnavailableValue { get; } /// /// Refreshes the setting to account for possible /// changes that could affect it's functionality. /// /// A bool that determines whether the /// setting's value was changed. bool RefreshSetting(); } } ================================================ FILE: ClientGUI/Settings/IUserSetting.cs ================================================ namespace ClientGUI.Settings { public interface IUserSetting { /// /// INI section name in user settings file this setting's value is stored in. /// string SettingSection { get; } /// /// INI key name in user settings file this setting's value is stored in. /// string SettingKey { get; } /// /// Determines if this setting requires the client to be restarted /// in order to be correctly applied. /// bool RestartRequired { get; } /// /// Determines if the setting should reset to its default value where applicable once game process exits. /// public bool ResetToDefaultOnGameExit { get; } /// /// Loads the current value for the user setting. /// void Load(); /// /// Applies operations based on current setting state. /// /// A bool that determines whether the /// client needs to restart for changes to apply. bool Save(); } } ================================================ FILE: ClientGUI/Settings/SettingCheckBox.cs ================================================ using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; namespace ClientGUI.Settings { /// /// A check-box for toggling options in user settings INI file. /// public class SettingCheckBox : SettingCheckBoxBase { public SettingCheckBox(WindowManager windowManager) : base(windowManager) { } public SettingCheckBox(WindowManager windowManager, bool defaultValue, string settingSection, string settingKey, bool writeSettingValue = false, string enabledValue = "", string disabledValue = "", bool restartRequired = false) : base(windowManager, defaultValue, settingSection, settingKey, restartRequired) { WriteSettingValue = writeSettingValue; EnabledSettingValue = enabledValue; DisabledSettingValue = disabledValue; } private bool _writeSettingValue; /// /// If set, use separate enabled / disabled values instead of checkbox's checked state when reading & writing setting to the user settings INI. /// public bool WriteSettingValue { get => _writeSettingValue; set { _writeSettingValue = value; defaultKeySuffix = _writeSettingValue ? "_Value" : "_Checked"; } } /// /// Value to write instead of true when checkbox is enabled. /// public string EnabledSettingValue { get; set; } = string.Empty; /// /// Value to write instead of false when checkbox is disabled. /// public string DisabledSettingValue { get; set; } = string.Empty; protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "WriteSettingValue": WriteSettingValue = Conversions.BooleanFromString(value, false); return; case "EnabledSettingValue": EnabledSettingValue = value; return; case "DisabledSettingValue": DisabledSettingValue = value; return; } base.ParseControlINIAttribute(iniFile, key, value); } public override void Load() { string value = UserINISettings.Instance.GetValue(SettingSection, SettingKey, string.Empty); if (WriteSettingValue) { if (value == EnabledSettingValue) Checked = true; else if (value == DisabledSettingValue) Checked = false; else Checked = DefaultValue; } else Checked = Conversions.BooleanFromString(value, DefaultValue); originalState = Checked; } public override bool Save() { if (WriteSettingValue) UserINISettings.Instance.SetValue(SettingSection, SettingKey, Checked ? EnabledSettingValue : DisabledSettingValue); else UserINISettings.Instance.SetValue(SettingSection, SettingKey, Checked); return RestartRequired && (Checked != originalState); } } } ================================================ FILE: ClientGUI/Settings/SettingCheckBoxBase.cs ================================================ using System; using Rampastring.Tools; using Rampastring.XNAUI; namespace ClientGUI.Settings { public abstract class SettingCheckBoxBase : XNAClientCheckBox, IUserSetting { public SettingCheckBoxBase(WindowManager windowManager) : base(windowManager) { } public SettingCheckBoxBase(WindowManager windowManager, bool defaultValue, string settingSection, string settingKey, bool restartRequired = false, bool resetPerGameSession = false) : base(windowManager) { DefaultValue = defaultValue; SettingSection = settingSection; SettingKey = settingKey; RestartRequired = restartRequired; ResetToDefaultOnGameExit = resetPerGameSession; } public bool DefaultValue { get; set; } private string _settingSection; public string SettingSection { get => string.IsNullOrEmpty(_settingSection) ? defaultSection : _settingSection; set => _settingSection = value; } private string _settingKey; public string SettingKey { get => string.IsNullOrEmpty(_settingKey) ? $"{Name}{defaultKeySuffix}" : _settingKey; set => _settingKey = value; } public bool RestartRequired { get; set; } public bool ResetToDefaultOnGameExit { get; set; } private string _parentCheckBoxName; /// /// Name of parent check-box control. /// public string ParentCheckBoxName { get { return _parentCheckBoxName; } set { _parentCheckBoxName = value; UpdateParentCheckBox(FindParentCheckBox()); } } private XNAClientCheckBox _parentCheckBox; /// /// Parent check-box control. /// public XNAClientCheckBox ParentCheckBox { get { return _parentCheckBox; } set { UpdateParentCheckBox(value); _parentCheckBoxName = _parentCheckBox != null ? _parentCheckBox.Name : null; } } /// /// Value required from parent check-box control if set. /// public bool ParentCheckBoxRequiredValue { get; set; } = true; protected string defaultSection = "CustomSettings"; protected string defaultKeySuffix = "_Checked"; protected bool originalState; protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "Checked": case "DefaultValue": DefaultValue = Conversions.BooleanFromString(value, false); return; case "SettingSection": SettingSection = string.IsNullOrEmpty(value) ? SettingSection : value; return; case "SettingKey": SettingKey = string.IsNullOrEmpty(value) ? SettingKey : value; return; case "RestartRequired": RestartRequired = Conversions.BooleanFromString(value, false); return; case "ParentCheckBoxName": ParentCheckBoxName = value; return; case "ParentCheckBoxRequiredValue": ParentCheckBoxRequiredValue = Conversions.BooleanFromString(value, true); return; case "ResetToDefaultOnGameExit": ResetToDefaultOnGameExit = Conversions.BooleanFromString(value, false); return; } base.ParseControlINIAttribute(iniFile, key, value); } public abstract void Load(); public abstract bool Save(); private XNAClientCheckBox FindParentCheckBox() { if (string.IsNullOrEmpty(ParentCheckBoxName)) return null; foreach (var control in Parent.Children) { if (control is XNAClientCheckBox && control.Name == ParentCheckBoxName) return control as XNAClientCheckBox; } return null; } private void UpdateParentCheckBox(XNAClientCheckBox parentCheckBox) { if (ParentCheckBox != null) ParentCheckBox.CheckedChanged -= ParentCheckBox_CheckedChanged; _parentCheckBox = parentCheckBox; UpdateAllowChecking(); if (ParentCheckBox != null) ParentCheckBox.CheckedChanged += ParentCheckBox_CheckedChanged; } private void ParentCheckBox_CheckedChanged(object sender, EventArgs e) => UpdateAllowChecking(); private void UpdateAllowChecking() { if (ParentCheckBox != null) { if (ParentCheckBox.Checked == ParentCheckBoxRequiredValue) { AllowChecking = true; } else { AllowChecking = false; Checked = false; } } } } } ================================================ FILE: ClientGUI/Settings/SettingDropDown.cs ================================================ using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; namespace ClientGUI.Settings { /// /// Dropdown for toggling options in user settings INI file. /// public class SettingDropDown : SettingDropDownBase { public SettingDropDown(WindowManager windowManager) : base(windowManager) { } public SettingDropDown(WindowManager windowManager, int defaultValue, string settingSection, string settingKey, bool writeItemValue = false, bool restartRequired = false) : base(windowManager, defaultValue, settingSection, settingKey, restartRequired) { WriteItemValue = writeItemValue; } private bool _writeItemValue; /// /// If set, dropdown item's value instead of index is written to the user settings INI. /// public bool WriteItemValue { get => _writeItemValue; set { _writeItemValue = value; defaultKeySuffix = _writeItemValue ? "_Value" : "_SelectedIndex"; } } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "WriteItemValue": WriteItemValue = Conversions.BooleanFromString(value, false); return; } base.ParseControlINIAttribute(iniFile, key, value); } public override void Load() { if (WriteItemValue) SelectedIndex = FindItemIndexByValue(UserINISettings.Instance.GetValue(SettingSection, SettingKey, null)); else SelectedIndex = UserINISettings.Instance.GetValue(SettingSection, SettingKey, DefaultValue); originalState = SelectedIndex; } public override bool Save() { if (WriteItemValue) UserINISettings.Instance.SetValue(SettingSection, SettingKey, (string)SelectedItem.Tag); else UserINISettings.Instance.SetValue(SettingSection, SettingKey, SelectedIndex); return RestartRequired && (SelectedIndex != originalState); } private int FindItemIndexByValue(string value) { if (string.IsNullOrEmpty(value)) return DefaultValue; int index = Items.FindIndex(x => (string)x.Tag == value); if (index < 0) return DefaultValue; return index; } } } ================================================ FILE: ClientGUI/Settings/SettingDropDownBase.cs ================================================ using ClientCore.I18N; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace ClientGUI.Settings { public abstract class SettingDropDownBase : XNAClientDropDown, IUserSetting { public SettingDropDownBase(WindowManager windowManager) : base(windowManager) { } public SettingDropDownBase(WindowManager windowManager, int defaultValue, string settingSection, string settingKey, bool restartRequired = false) : base(windowManager) { DefaultValue = defaultValue; SettingSection = settingSection; SettingKey = settingKey; RestartRequired = restartRequired; } public int DefaultValue { get; set; } private string _settingSection; public string SettingSection { get => string.IsNullOrEmpty(_settingSection) ? defaultSection : _settingSection; set => _settingSection = value; } private string _settingKey; public string SettingKey { get => string.IsNullOrEmpty(_settingKey) ? $"{Name}{defaultKeySuffix}" : _settingKey; set => _settingKey = value; } public bool RestartRequired { get; set; } public bool ResetToDefaultOnGameExit { get; set; } protected string defaultSection = "CustomSettings"; protected string defaultKeySuffix = "_SelectedIndex"; protected int originalState; protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { // shorthand for localization function static string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true) => Translation.Instance.LookUp(control, attributeName, defaultValue, notify); switch (key) { case "Items": string[] items = value.Split(','); for (int i = 0; i < items.Length; i++) { XNADropDownItem item = new XNADropDownItem { Text = Localize(this, $"Item{i}", items[i]), Tag = items[i] }; AddItem(item); } return; case "DefaultValue": DefaultValue = Conversions.IntFromString(value, 0); return; case "SettingSection": SettingSection = string.IsNullOrEmpty(value) ? SettingSection : value; return; case "SettingKey": SettingKey = string.IsNullOrEmpty(value) ? SettingKey : value; return; case "RestartRequired": RestartRequired = Conversions.BooleanFromString(value, false); return; } base.ParseControlINIAttribute(iniFile, key, value); } public abstract void Load(); public abstract bool Save(); } } ================================================ FILE: ClientGUI/ToolTip.cs ================================================ using ClientCore; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace ClientGUI { /// /// A tool tip. /// public class ToolTip : XNAControl { /// /// If set to true - makes tooltip not appear and instantly hides it if currently shown. /// public bool Blocked { get; set; } /// /// Whether the tooltip should move with the cursor after it was shown. /// public bool FollowCursor { get; set; } /// /// Creates a new tool tip and attaches it to the given control. /// /// The window manager. /// The control to attach the tool tip to. public ToolTip(WindowManager windowManager, XNAControl masterControl) : base(windowManager) { this.masterControl = masterControl ?? throw new ArgumentNullException("masterControl"); masterControl.MouseEnter += MasterControl_MouseEnter; masterControl.MouseLeave += MasterControl_MouseLeave; masterControl.MouseMove += MasterControl_MouseMove; masterControl.EnabledChanged += MasterControl_EnabledChanged; InputEnabled = false; DrawOrder = int.MaxValue; GetParentControl(masterControl.Parent).AddChild(this); Visible = false; } private XNAControl GetParentControl(XNAControl parent) { if (parent is XNAWindow) return parent as XNAWindow; else if (parent is INItializableWindow) return parent as INItializableWindow; else if (parent.Parent != null) return GetParentControl(parent.Parent); else return parent; } private void MasterControl_EnabledChanged(object sender, EventArgs e) => Enabled = masterControl.Enabled; public override string Text { get => base.Text; set { base.Text = value; Vector2 textSize = Renderer.GetTextDimensions(base.Text ?? string.Empty, ClientConfiguration.Instance.ToolTipFontIndex); Width = (int)textSize.X + ClientConfiguration.Instance.ToolTipMargin * 2; Height = (int)textSize.Y + ClientConfiguration.Instance.ToolTipMargin * 2; if (string.IsNullOrEmpty(Text)) { Alpha = 0f; Visible = false; } } } public override float Alpha { get; set; } public bool IsMasterControlOnCursor { get; set; } private XNAControl masterControl; private TimeSpan cursorTime = TimeSpan.Zero; private void MasterControl_MouseEnter(object sender, EventArgs e) { IsMasterControlOnCursor = true; if (string.IsNullOrEmpty(Text)) return; DisplayAtLocation(SumPoints(WindowManager.Cursor.Location, new Point(ClientConfiguration.Instance.ToolTipOffsetX, ClientConfiguration.Instance.ToolTipOffsetY))); } private void MasterControl_MouseLeave(object sender, EventArgs e) { IsMasterControlOnCursor = false; cursorTime = TimeSpan.Zero; } private void MasterControl_MouseMove(object sender, EventArgs e) { if ((FollowCursor || !Visible) && !string.IsNullOrEmpty(Text)) { // Move the tooltip if the cursor has moved while staying // on the control area and we're invisible or we follow the cursor DisplayAtLocation(SumPoints(WindowManager.Cursor.Location, new Point(ClientConfiguration.Instance.ToolTipOffsetX, ClientConfiguration.Instance.ToolTipOffsetY))); } } /// /// Sets the tool tip's location, checking that it doesn't exceed the window's bounds. /// /// The point at location coordinates. public void DisplayAtLocation(Point location) { X = location.X + Width > WindowManager.RenderResolutionX ? WindowManager.RenderResolutionX - Width : location.X; Y = location.Y - Height < 0 ? 0 : location.Y - Height; } public override void Update(GameTime gameTime) { if (Blocked || string.IsNullOrEmpty(Text)) { Alpha = 0f; Visible = false; return; } if (IsMasterControlOnCursor) { cursorTime += gameTime.ElapsedGameTime; if (cursorTime > TimeSpan.FromSeconds(ClientConfiguration.Instance.ToolTipDelay)) { Alpha += ClientConfiguration.Instance.ToolTipAlphaRatePerSecond * (float)gameTime.ElapsedGameTime.TotalSeconds; Visible = true; if (Alpha > 1.0f) Alpha = 1.0f; return; } } Alpha -= ClientConfiguration.Instance.ToolTipAlphaRatePerSecond * (float)gameTime.ElapsedGameTime.TotalSeconds; if (Alpha < 0f) { Alpha = 0f; Visible = false; } } public override void Draw(GameTime gameTime) { Renderer.FillRectangle(ClientRectangle, UISettings.ActiveSettings.BackgroundColor * Alpha); Renderer.DrawRectangle(ClientRectangle, UISettings.ActiveSettings.AltColor * Alpha); Renderer.DrawString(Text, ClientConfiguration.Instance.ToolTipFontIndex, new Vector2(X + ClientConfiguration.Instance.ToolTipMargin, Y + ClientConfiguration.Instance.ToolTipMargin), UISettings.ActiveSettings.AltColor * Alpha, 1.0f); } private Point SumPoints(Point p1, Point p2) // This is also needed for XNA compatibility #if XNA => new Point(p1.X + p2.X, p1.Y + p2.Y); #else => p1 + p2; #endif } } ================================================ FILE: ClientGUI/TranslationGUIExtensions.cs ================================================ using ClientCore.I18N; using Rampastring.XNAUI.XNAControls; namespace ClientGUI; public static class TranslationGUIExtensions { /// /// Looks up the translated value that corresponds to the given INI-defined control attribute. /// /// The control to look up the attribute value for. /// The attribute name as written in the INI. /// 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 static string LookUp(this Translation @this, XNAControl control, string attributeName, string defaultValue, bool notify = true) { string key = $"INI:Controls:{control.Parent?.Name ?? "Global"}:{control.Name}:{attributeName}"; string globalKey = $"INI:Controls:Global:{control.Name}:{attributeName}"; return @this.LookUp(key, fallbackKey: globalKey, defaultValue, notify); } } ================================================ FILE: ClientGUI/TranslationINIParser.cs ================================================ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using ClientCore; using ClientCore.Extensions; using ClientCore.I18N; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI.XNAControls; namespace ClientGUI; public class TranslationINIParser : IControlINIAttributeParser { private static TranslationINIParser _instance; public static TranslationINIParser Instance => _instance ??= new TranslationINIParser(); // shorthand for localization function private string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true) => Translation.Instance.LookUp(control, attributeName, defaultValue, notify); public bool ParseINIAttribute(XNAControl control, IniFile iniFile, string key, string value) { switch (key) { case "Text": control.Text = Localize(control, key, value.FromIniString()); return true; case "Size": string[] size = Localize(control, key, value, notify: false).Split(','); control.ClientRectangle = new Rectangle(control.X, control.Y, int.Parse(size[0], CultureInfo.InvariantCulture), int.Parse(size[1], CultureInfo.InvariantCulture)); return true; case "Width": control.Width = int.Parse(Localize(control, key, value, notify: false), CultureInfo.InvariantCulture); return true; case "Height": control.Height = int.Parse(Localize(control, key, value, notify: false), CultureInfo.InvariantCulture); return true; case "Location": string[] location = Localize(control, key, value, notify: false).Split(','); control.ClientRectangle = new Rectangle( int.Parse(location[0], CultureInfo.InvariantCulture), int.Parse(location[1], CultureInfo.InvariantCulture), control.Width, control.Height); return true; case "X": control.X = int.Parse(Localize(control, key, value, notify: false), CultureInfo.InvariantCulture); return true; case "Y": control.Y = int.Parse(Localize(control, key, value, notify: false), CultureInfo.InvariantCulture); return true; case "DistanceFromRightBorder": if (control.Parent != null) { control.ClientRectangle = new Rectangle( control.Parent.Width - control.Width - Conversions.IntFromString(Localize(control, key, value, notify: false), 0), control.Y, control.Width, control.Height); } return true; case "DistanceFromBottomBorder": if (control.Parent != null) { control.ClientRectangle = new Rectangle( control.X, control.Parent.Height - control.Height - Conversions.IntFromString(Localize(control, key, value, notify: false), 0), control.Width, control.Height); } return true; case "ToolTip" when control is IToolTipContainer controlWithToolTip: controlWithToolTip.ToolTipText = Localize(control, key, value.FromIniString()); return true; case "Suggestion" when control is XNASuggestionTextBox suggestionTextBox: suggestionTextBox.Suggestion = Localize(control, key, value.FromIniString()); return true; case "URL" when control is XNALinkButton button: // need to link localized docs button.URL = Localize(control, key, value.FromIniString(), notify: false); return true; case "UnixURL" when control is XNALinkButton button: button.UnixURL = Localize(control, key, value.FromIniString(), notify: false); return true; } return false; } } ================================================ FILE: ClientGUI/UIDesignConstants.cs ================================================ namespace ClientGUI { /// /// Contains constants used in user interface design. /// public static class UIDesignConstants { public const int EMPTY_SPACE_SIDES = 6; public const int EMPTY_SPACE_TOP = 6; public const int EMPTY_SPACE_BOTTOM = 6; public const int CONTROL_VERTICAL_MARGIN = 6; public const int CONTROL_HORIZONTAL_MARGIN = 6; public const int BUTTON_HEIGHT = 23; public const int BUTTON_WIDTH_75 = 75; public const int BUTTON_WIDTH_92 = 92; public const int BUTTON_WIDTH_121 = 121; public const int BUTTON_WIDTH_133 = 133; public const int BUTTON_WIDTH_160 = 160; } } ================================================ FILE: ClientGUI/XNAChatTextBox.cs ================================================ using Microsoft.Xna.Framework.Input; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; namespace ClientGUI { /// /// A text box that stores entered messages and allows viewing them /// with the arrow keys. /// public class XNAChatTextBox : XNASuggestionTextBox { public XNAChatTextBox(WindowManager windowManager) : base(windowManager) { EnterPressed += XNAChatTextBox_EnterPressed; } private LinkedList enteredMessages = new LinkedList(); private LinkedListNode currentNode; private void XNAChatTextBox_EnterPressed(object sender, EventArgs e) { if (!string.IsNullOrEmpty(Text)) enteredMessages.AddFirst(Text); } protected override bool HandleKeyPress(Keys key) { if (key == Keys.Up) { if (currentNode == null) { if (enteredMessages.First != null) currentNode = enteredMessages.First; } else { if (currentNode.Next != null) currentNode = currentNode.Next; } if (currentNode != null) Text = currentNode.Value; return true; } if (key == Keys.Down) { if (currentNode != null && currentNode.Previous != null) { currentNode = currentNode.Previous; Text = currentNode.Value; } return true; } currentNode = null; return base.HandleKeyPress(key); } } } ================================================ FILE: ClientGUI/XNAClientButton.cs ================================================ using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; using Rampastring.Tools; using System; using ClientCore; using ClientCore.Extensions; namespace ClientGUI { public class XNAClientButton : XNAButton, IToolTipContainer { public ToolTip ToolTip { get; private set; } private string _initialToolTipText; public string ToolTipText { get => Initialized ? ToolTip?.Text : _initialToolTipText; set { if (Initialized) ToolTip.Text = value; else _initialToolTipText = value; } } public XNAClientButton(WindowManager windowManager) : base(windowManager) { FontIndex = 1; Height = UIDesignConstants.BUTTON_HEIGHT; } public override void Initialize() { int width = Width; if (IdleTexture == null) IdleTexture = AssetLoader.LoadTexture(width + "pxbtn.png"); if (HoverTexture == null) HoverTexture = AssetLoader.LoadTexture(width + "pxbtn_c.png"); if (HoverSoundEffect == null) HoverSoundEffect = new EnhancedSoundEffect("button.wav"); base.Initialize(); if (Width == 0) Width = IdleTexture.Width; ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText }; } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "MatchTextureSize" && Conversions.BooleanFromString(value, false)) { Width = IdleTexture.Width; Height = IdleTexture.Height; return; } else if (key == "ToolTip") { ToolTipText = value.FromIniString(); return; } base.ParseControlINIAttribute(iniFile, key, value); } } } ================================================ FILE: ClientGUI/XNAClientCheckBox.cs ================================================ using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; using System; using Rampastring.Tools; using ClientCore; using ClientCore.Extensions; namespace ClientGUI { public class XNAClientCheckBox : XNACheckBox, IToolTipContainer { public ToolTip ToolTip { get; private set; } private string _initialToolTipText; public string ToolTipText { get => Initialized ? ToolTip?.Text : _initialToolTipText; set { if (Initialized) ToolTip.Text = value; else _initialToolTipText = value; } } public XNAClientCheckBox(WindowManager windowManager) : base(windowManager) { } public override void Initialize() { CheckSoundEffect = new EnhancedSoundEffect("checkbox.wav"); base.Initialize(); ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText }; } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "ToolTip") { ToolTipText = value.FromIniString(); return; } base.ParseControlINIAttribute(iniFile, key, value); } } } ================================================ FILE: ClientGUI/XNAClientColorDropDown.cs ================================================ using System.Collections.Generic; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace ClientGUI { public class XNAClientColorDropDown : XNAClientDropDown { private const int VERTICAL_PADDING = 3; private const int HORIZONTAL_PADDING = 2; public ItemsKind ItemsDrawMode { get; private set; } = ItemsKind.TextAndIcon; public int ColorTextureWidth { get; private set; } public int ColorTextureHeight { get; private set; } public Texture2D RandomColorTexture { get; private set; } public Texture2D DisabledItemTexture { get; private set; } private Dictionary itemColorTextures = new Dictionary(); public XNAClientColorDropDown(WindowManager windowManager) : base(windowManager) { ColorTextureWidth = Height - VERTICAL_PADDING; ColorTextureHeight = Height - HORIZONTAL_PADDING; RandomColorTexture = AssetLoader.LoadTexture("randomicon.png"); DisabledItemTexture = AssetLoader.CreateTexture(DisabledItemColor, ColorTextureWidth, ColorTextureHeight); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case nameof(ItemsDrawMode): ItemsDrawMode = value.FromIniString().ToEnum(); switch (ItemsDrawMode) { case ItemsKind.Text: for (int i = 0; i < Items.Count; i++) { // Text mode: use transparent 1x1 texture as placeholder var texture = AssetLoader.CreateTexture(AssetLoader.GetRGBAColorFromString("0,0,0,0"), 1, 1); Items[i].Texture = texture; itemColorTextures[i] = texture; } break; case ItemsKind.Icon: ColorTextureWidth = Width - VERTICAL_PADDING; ColorTextureHeight = Height - HORIZONTAL_PADDING; for (int i = 0; i < Items.Count; i++) { if (i != 0) // Skip random color item { var texture = AssetLoader.CreateTexture( Items[i].TextColor ?? Color.White, ColorTextureWidth, ColorTextureHeight); Items[i].Texture = texture; itemColorTextures[i] = texture; } Items[i].Text = string.Empty; } DisabledItemTexture = AssetLoader.CreateTexture(DisabledItemColor, Width - VERTICAL_PADDING, Height - HORIZONTAL_PADDING); break; case ItemsKind.TextAndIcon: break; default: break; } return; case nameof(ColorTextureWidth): ColorTextureWidth = Conversions.IntFromString(value, ColorTextureWidth); break; case nameof(ColorTextureHeight): ColorTextureHeight = Conversions.IntFromString(value, ColorTextureHeight); break; case nameof(RandomColorTexture): RandomColorTexture = AssetLoader.LoadTexture(value); Items[0].Texture = RandomColorTexture; break; case nameof(DisabledItemTexture): DisabledItemTexture = AssetLoader.LoadTexture(value); break; default: base.ParseControlINIAttribute(iniFile, key, value); return; } } public new virtual void AddItem(string text, Color color) { var item = new XNADropDownItem(); item.Text = text; item.TextColor = color; int index = Items.Count; if (index > 0) // Not the random color item { var texture = AssetLoader.CreateTexture(color, ColorTextureWidth, ColorTextureHeight); item.Texture = texture; itemColorTextures[index] = texture; } else { item.Texture = RandomColorTexture; } Items.Add(item); } /// /// Enables or disables the color texture for an item by swapping between the color texture and disabled texture. /// /// The index of the item. /// If true, sets the color texture. If false, sets the disabled texture. public void SetItemColorEnabled(int itemIndex, bool enabled) { if (itemIndex < 0 || itemIndex >= Items.Count) return; // Skip random color if (itemIndex == 0) return; // Skip if in text only mode if (ItemsDrawMode == ItemsKind.Text) return; if (enabled) { if (itemColorTextures.TryGetValue(itemIndex, out var colorTexture)) Items[itemIndex].Texture = colorTexture; } else { Items[itemIndex].Texture = DisabledItemTexture; } } public enum ItemsKind { Text, Icon, TextAndIcon } } } ================================================ FILE: ClientGUI/XNAClientDropDown.cs ================================================ using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; using Rampastring.Tools; using System; using ClientCore; using ClientCore.Extensions; namespace ClientGUI { public class XNAClientDropDown : XNADropDown, IToolTipContainer { public ToolTip ToolTip { get; private set; } private string _initialToolTipText; public string ToolTipText { get => Initialized ? ToolTip?.Text : _initialToolTipText; set { if (Initialized) ToolTip.Text = value; else _initialToolTipText = value; } } public XNAClientDropDown(WindowManager windowManager) : base(windowManager) { } public override void Initialize() { ClickSoundEffect = new EnhancedSoundEffect("dropdown.wav"); base.Initialize(); ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText }; } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "ToolTip") { ToolTipText = value.FromIniString(); return; } base.ParseControlINIAttribute(iniFile, key, value); } public override void OnMouseLeftDown(InputEventArgs inputEventArgs) { // no need to set Handled to true since we're not "consuming" the event here, just augmenting base.OnMouseLeftDown(inputEventArgs); UpdateToolTipBlock(); } protected override void CloseDropDown() { base.CloseDropDown(); UpdateToolTipBlock(); } protected void UpdateToolTipBlock() { if (DropDownState == DropDownState.CLOSED) ToolTip.Blocked = false; else ToolTip.Blocked = true; } } } ================================================ FILE: ClientGUI/XNAClientLinkLabel.cs ================================================ using Rampastring.XNAUI; using Rampastring.Tools; using ClientCore; using Rampastring.XNAUI.XNAControls; using ClientCore.Extensions; using Microsoft.Xna.Framework; namespace ClientGUI { /// /// Link label with customizable URL and tooltip text as well as hover/click sounds. /// Also uses hover text color by default. /// public class XNAClientLinkLabel : XNALinkLabel, IToolTipContainer { public EnhancedSoundEffect HoverSoundEffect { get; set; } public EnhancedSoundEffect ClickSoundEffect { get; set; } private Color? _hoverColor; /// /// The color of the label when it's hovered on. /// public new Color HoverColor { get { return _hoverColor ?? UISettings.ActiveSettings.ButtonHoverColor; } set { _hoverColor = value; if (IsActive) RemapColor = value; } } public ToolTip ToolTip { get; private set; } private string _initialToolTipText; public string ToolTipText { get => Initialized ? ToolTip?.Text : _initialToolTipText; set { if (Initialized) ToolTip.Text = value; else _initialToolTipText = value; } } public XNAClientLinkLabel(WindowManager windowManager) : base(windowManager) { } public string URL { get; set; } public string UnixURL { get; set; } public override void Initialize() { base.Initialize(); ToolTip = new ToolTip(WindowManager, this) { Text = _initialToolTipText }; } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "ToolTip": ToolTipText = value.FromIniString(); return; case "URL": URL = value; return; case "UnixURL": UnixURL = value; return; case "HoverSoundEffect": HoverSoundEffect = new EnhancedSoundEffect(value); return; case "ClickSoundEffect": ClickSoundEffect = new EnhancedSoundEffect(value); return; } base.ParseControlINIAttribute(iniFile, key, value); } public override void OnMouseEnter() { base.OnMouseLeave(); HoverSoundEffect?.Play(); RemapColor = HoverColor; TextColor = HoverColor; } public override void OnMouseLeave() { base.OnMouseLeave(); RemapColor = IdleColor; TextColor = IdleColor; } public override void OnLeftClick(InputEventArgs inputEventArgs) { inputEventArgs.Handled = true; ClickSoundEffect?.Play(); OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); if (osVersion == OSVersion.UNIX && !string.IsNullOrEmpty(UnixURL)) ProcessLauncher.StartShellProcess(UnixURL); else if (!string.IsNullOrEmpty(URL)) ProcessLauncher.StartShellProcess(URL); base.OnLeftClick(inputEventArgs); } } } ================================================ FILE: ClientGUI/XNAClientPreferredItemDropDown.cs ================================================ using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System.Collections.Generic; namespace ClientGUI { /// /// A drop-down control that has a preferred drop-down item with an optional string label displayed next to its text. /// public class XNAClientPreferredItemDropDown : XNAClientDropDown { /// /// String label displayed next to the preferred drop-down item text. /// public string PreferredItemLabel { get; set; } /// /// Index of the preferred drop-down item. /// public List PreferredItemIndexes { get; set; } = new List(); /// /// Creates a new preferred item drop-down control. /// /// The WindowManager associated with this control. public XNAClientPreferredItemDropDown(WindowManager windowManager) : base(windowManager) { } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "PreferredItemLabel": PreferredItemLabel = value; return; } base.ParseControlINIAttribute(iniFile, key, value); } /// /// Draws the drop-down. /// public override void Draw(GameTime gameTime) { if (PreferredItemIndexes.Count > 0) { PreferredItemIndexes.ForEach(i => { XNADropDownItem preferredItem = Items[i]; string preferredItemOriginalText = preferredItem.Text; preferredItem.Text += " " + PreferredItemLabel; }); base.Draw(gameTime); PreferredItemIndexes.ForEach(i => { XNADropDownItem preferredItem = Items[i]; preferredItem.Text = preferredItem.Text.Substring(0, preferredItem.Text.Length - PreferredItemLabel.Length - 1); }); } else { base.Draw(gameTime); } } } } ================================================ FILE: ClientGUI/XNAClientStateButton.cs ================================================ using System; using System.Collections.Generic; using ClientCore.Extensions; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace ClientGUI { public class XNAClientStateButton : XNAButton where T : Enum { private T _state { get; set; } private Dictionary StateTextures { get; set; } private string _toolTipText { get; set; } private ToolTip _toolTip { get; set; } public XNAClientStateButton(WindowManager windowManager, Dictionary textures) : base(windowManager) { LeftClick += CycleState; StateTextures = textures; } public override void Initialize() { if (StateTextures == null || StateTextures.Count < 2) throw new ArgumentException("State button requires at least 2 states"); UpdateStateTexture(); base.Initialize(); _toolTip = new ToolTip(WindowManager, this); SetToolTipText(_toolTipText); if (Width == 0) Width = IdleTexture.Width; } public void SetState(T state) { if(!Enum.IsDefined(typeof(T), state)) throw new IndexOutOfRangeException($"{state} not a valid texture value"); _state = state; UpdateStateTexture(); } public T GetState() => _state; private void CycleState(object sender, EventArgs e) { _state = _state.CycleNext(); UpdateStateTexture(); } public void SetToolTipText(string text) { _toolTipText = text ?? string.Empty; if (_toolTip != null) _toolTip.Text = _toolTipText; } private void UpdateStateTexture() { IdleTexture = StateTextures[_state]; } } } ================================================ FILE: ClientGUI/XNAClientTabControl.cs ================================================ using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; namespace ClientGUI { public class XNAClientTabControl : XNATabControl { public XNAClientTabControl(WindowManager windowManager) : base(windowManager) { } public override void Initialize() { if (ClickSound == null) { ClickSound = new EnhancedSoundEffect("button.wav"); } base.Initialize(); } public void AddTab(string text, int width) { string tabAssetName = width + "pxtab"; if (AssetLoader.AssetExists(tabAssetName + ".png")) { AddTab(text, AssetLoader.LoadTexture(tabAssetName + ".png"), AssetLoader.LoadTexture(tabAssetName + "_c.png")); } else { AddTab(text, AssetLoader.LoadTexture(width + "pxbtn.png"), AssetLoader.LoadTexture(width + "pxbtn_c.png")); } } } } ================================================ FILE: ClientGUI/XNAClientToggleButton.cs ================================================ using System; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace ClientGUI { /// /// This is a combination of a checkbox and a standard button. You must specify /// the Checked and Unchecked Textures to render for each button state. /// public class XNAClientToggleButton : XNAButton { public Texture2D CheckedTexture { get; set; } public Texture2D UncheckedTexture { get; set; } private string _toolTipText { get; set; } private ToolTip ToolTip { get; set; } private bool _checked { get; set; } public override void Initialize() { if (CheckedTexture == null) throw new ArgumentNullException(nameof(CheckedTexture)); if (UncheckedTexture == null) throw new ArgumentNullException(nameof(UncheckedTexture)); UpdateIdleTexture(); if (HoverSoundEffect == null) HoverSoundEffect = new EnhancedSoundEffect("button.wav"); base.Initialize(); ToolTip = new ToolTip(WindowManager, this); SetToolTipText(_toolTipText); if (Width == 0) Width = IdleTexture.Width; } public bool Checked { get => _checked; set { _checked = value; UpdateIdleTexture(); } } private void UpdateIdleTexture() { IdleTexture = _checked ? CheckedTexture : UncheckedTexture; } public void SetToolTipText(string text) { _toolTipText = text ?? string.Empty; if (ToolTip != null) ToolTip.Text = _toolTipText; } public XNAClientToggleButton(WindowManager windowManager) : base(windowManager) { } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case nameof(CheckedTexture): CheckedTexture = AssetLoader.LoadTexture(value); UpdateIdleTexture(); break; case nameof(UncheckedTexture): UncheckedTexture = AssetLoader.LoadTexture(value); UpdateIdleTexture(); break; case nameof(ToolTip): SetToolTipText(value); break; default: base.ParseControlINIAttribute(iniFile, key, value); break; } } } } ================================================ FILE: ClientGUI/XNAExtraPanel.cs ================================================ using Microsoft.Xna.Framework; using Rampastring.XNAUI.XNAControls; using Rampastring.Tools; using Rampastring.XNAUI; namespace ClientGUI { /// /// An "extra panel" for modders that automatically /// changes its size to match the texture size. /// public class XNAExtraPanel : XNAPanel { public XNAExtraPanel(WindowManager windowManager) : base(windowManager) { InputEnabled = false; DrawBorders = false; } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "BackgroundTexture") { BackgroundTexture = AssetLoader.LoadTexture(value); if (new Point(Width, Height) == Point.Zero) { ClientRectangle = new Rectangle(X, Y, BackgroundTexture.Width, BackgroundTexture.Height); } return; } base.ParseControlINIAttribute(iniFile, key, value); } } } ================================================ FILE: ClientGUI/XNALinkButton.cs ================================================ using System; using Rampastring.XNAUI; using Rampastring.Tools; using ClientCore; namespace ClientGUI { public class XNALinkButton : XNAClientButton { public XNALinkButton(WindowManager windowManager) : base(windowManager) { } public string URL { get; set; } public string UnixURL { get; set; } public string Arguments { get; set; } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "URL") { URL = value; return; } if (key == "UnixURL") { UnixURL = value; return; } if (key == "Arguments") { Arguments = value; return; } base.ParseControlINIAttribute(iniFile, key, value); } public override void OnLeftClick(InputEventArgs inputEventArgs) { inputEventArgs.Handled = true; OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); if (osVersion == OSVersion.UNIX && !string.IsNullOrEmpty(UnixURL)) ProcessLauncher.StartShellProcess(UnixURL, Arguments); else if (!string.IsNullOrEmpty(URL)) ProcessLauncher.StartShellProcess(URL, Arguments); base.OnLeftClick(inputEventArgs); } } } ================================================ FILE: ClientGUI/XNAMessageBox.cs ================================================ using ClientCore.Extensions; using System; using Microsoft.Xna.Framework; using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; using Microsoft.Xna.Framework.Input; namespace ClientGUI { /// /// A generic message box with OK or Yes/No or OK/Cancel buttons. /// public class XNAMessageBox : XNAWindow { /// /// Creates a new message box. /// /// The window manager. /// The caption of the message box. /// The actual message of the message box. /// Defines which buttons are available in the dialog. public XNAMessageBox(WindowManager windowManager, string caption, string description, XNAMessageBoxButtons messageBoxButtons) : base(windowManager) { this.caption = caption; this.description = description; this.messageBoxButtons = messageBoxButtons; } /// /// The method that is called when the user clicks OK on the message box. /// public Action OKClickedAction { get; set; } /// /// The method that is called when the user clicks Yes on the message box. /// public Action YesClickedAction { get; set; } /// /// The method that is called when the user clicks No on the message box. /// public Action NoClickedAction { get; set; } /// /// The method that is called when the user clicks Cancel on the message box. /// public Action CancelClickedAction { get; set; } private string caption; private string description; private XNAMessageBoxButtons messageBoxButtons; public override void Initialize() { Name = "MessageBox"; BackgroundTexture = AssetLoader.LoadTexture("msgboxform.png"); XNALabel lblCaption = new XNALabel(WindowManager); lblCaption.Text = caption; lblCaption.ClientRectangle = new Rectangle(12, 9, 0, 0); lblCaption.FontIndex = 1; XNAPanel line = new XNAPanel(WindowManager); line.ClientRectangle = new Rectangle(6, 29, 0, 1); XNALabel lblDescription = new XNALabel(WindowManager); lblDescription.Text = description; lblDescription.ClientRectangle = new Rectangle(12, 39, 0, 0); AddChild(lblCaption); AddChild(line); AddChild(lblDescription); Vector2 textDimensions = Renderer.GetTextDimensions(lblDescription.Text, lblDescription.FontIndex); ClientRectangle = new Rectangle(0, 0, (int)textDimensions.X + 24, (int)textDimensions.Y + 81); line.ClientRectangle = new Rectangle(6, 29, Width - 12, 1); if (messageBoxButtons == XNAMessageBoxButtons.OK) { AddOKButton(); } else if (messageBoxButtons == XNAMessageBoxButtons.YesNo) { AddYesNoButtons(); } else // messageBoxButtons == DXMessageBoxButtons.OKCancel { AddOKCancelButtons(); } base.Initialize(); WindowManager.CenterControlOnScreen(this); } private void AddOKButton() { XNAButton btnOK = new XNAButton(WindowManager); btnOK.FontIndex = 1; btnOK.ClientRectangle = new Rectangle(0, 0, 75, 23); btnOK.IdleTexture = AssetLoader.LoadTexture("75pxbtn.png"); btnOK.HoverTexture = AssetLoader.LoadTexture("75pxbtn_c.png"); btnOK.HoverSoundEffect = new EnhancedSoundEffect("button.wav"); btnOK.Name = "btnOK"; btnOK.Text = "OK".L10N("Client:ClientGUI:ButtonOK"); btnOK.LeftClick += BtnOK_LeftClick; btnOK.HotKey = Keys.Enter; AddChild(btnOK); btnOK.CenterOnParent(); btnOK.ClientRectangle = new Rectangle(btnOK.X, Height - 28, btnOK.Width, btnOK.Height); } private void AddYesNoButtons() { XNAButton btnYes = new XNAButton(WindowManager); btnYes.FontIndex = 1; btnYes.ClientRectangle = new Rectangle(0, 0, 75, 23); btnYes.IdleTexture = AssetLoader.LoadTexture("75pxbtn.png"); btnYes.HoverTexture = AssetLoader.LoadTexture("75pxbtn_c.png"); btnYes.HoverSoundEffect = new EnhancedSoundEffect("button.wav"); btnYes.Name = "btnYes"; btnYes.Text = "Yes".L10N("Client:ClientGUI:ButtonYes"); btnYes.LeftClick += BtnYes_LeftClick; btnYes.HotKey = Keys.Y; AddChild(btnYes); btnYes.ClientRectangle = new Rectangle((Width - ((btnYes.Width + 5) * 2)) / 2, Height - 28, btnYes.Width, btnYes.Height); XNAButton btnNo = new XNAButton(WindowManager); btnNo.FontIndex = 1; btnNo.ClientRectangle = new Rectangle(0, 0, 75, 23); btnNo.IdleTexture = AssetLoader.LoadTexture("75pxbtn.png"); btnNo.HoverTexture = AssetLoader.LoadTexture("75pxbtn_c.png"); btnNo.HoverSoundEffect = new EnhancedSoundEffect("button.wav"); btnNo.Name = "btnNo"; btnNo.Text = "No".L10N("Client:ClientGUI:ButtonNo"); btnNo.LeftClick += BtnNo_LeftClick; btnNo.HotKey = Keys.N; AddChild(btnNo); btnNo.ClientRectangle = new Rectangle(btnYes.X + btnYes.Width + 10, Height - 28, btnNo.Width, btnNo.Height); } private void AddOKCancelButtons() { XNAButton btnOK = new XNAButton(WindowManager); btnOK.FontIndex = 1; btnOK.ClientRectangle = new Rectangle(0, 0, 75, 23); btnOK.IdleTexture = AssetLoader.LoadTexture("75pxbtn.png"); btnOK.HoverTexture = AssetLoader.LoadTexture("75pxbtn_c.png"); btnOK.HoverSoundEffect = new EnhancedSoundEffect("button.wav"); btnOK.Name = "btnOK"; btnOK.Text = "OK".L10N("Client:ClientGUI:ButtonOK"); btnOK.LeftClick += BtnYes_LeftClick; btnOK.HotKey = Keys.Enter; AddChild(btnOK); btnOK.ClientRectangle = new Rectangle((Width - ((btnOK.Width + 5) * 2)) / 2, Height - 28, btnOK.Width, btnOK.Height); XNAButton btnCancel = new XNAButton(WindowManager); btnCancel.FontIndex = 1; btnCancel.ClientRectangle = new Rectangle(0, 0, 75, 23); btnCancel.IdleTexture = AssetLoader.LoadTexture("75pxbtn.png"); btnCancel.HoverTexture = AssetLoader.LoadTexture("75pxbtn_c.png"); btnCancel.HoverSoundEffect = new EnhancedSoundEffect("button.wav"); btnCancel.Name = "btnCancel"; btnCancel.Text = "Cancel".L10N("Client:ClientGUI:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; btnCancel.HotKey = Keys.C; AddChild(btnCancel); btnCancel.ClientRectangle = new Rectangle(btnOK.X + btnOK.Width + 10, Height - 28, btnCancel.Width, btnCancel.Height); } private void BtnOK_LeftClick(object sender, EventArgs e) { Hide(); OKClickedAction?.Invoke(this); } private void BtnYes_LeftClick(object sender, EventArgs e) { Hide(); YesClickedAction?.Invoke(this); } private void BtnNo_LeftClick(object sender, EventArgs e) { Hide(); NoClickedAction?.Invoke(this); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Hide(); CancelClickedAction?.Invoke(this); } private void Hide() { if (this.Parent != null) WindowManager.RemoveControl(this.Parent); else WindowManager.RemoveControl(this); } public void Show() { DarkeningPanel.AddAndInitializeWithControl(WindowManager, this); } #region Static Show methods /// /// Creates and displays a new message box with the specified caption and description. /// /// The game. /// The caption/header of the message box. /// The description of the message box. public static void Show(WindowManager windowManager, string caption, string description) { var panel = new DarkeningPanel(windowManager) { Focused = true }; windowManager.AddAndInitializeControl(panel); var msgBox = new XNAMessageBox(windowManager, Renderer.GetSafeString(caption, 1), Renderer.GetSafeString(description, 0), XNAMessageBoxButtons.OK); panel.AddChild(msgBox); msgBox.OKClickedAction = MsgBox_OKClicked; windowManager.AddAndInitializeControl(msgBox); windowManager.SelectedControl = null; } private static void MsgBox_OKClicked(XNAMessageBox messageBox) { var parent = (DarkeningPanel)messageBox.Parent; parent.Hide(); parent.Hidden += Parent_Hidden; } /// /// Shows a message box with "Yes" and "No" being the user input options. /// /// The WindowManager. /// The caption of the message box. /// The description in the message box. /// The XNAMessageBox instance that is created. public static XNAMessageBox ShowYesNoDialog(WindowManager windowManager, string caption, string description) { var panel = new DarkeningPanel(windowManager) { Focused = true }; windowManager.AddAndInitializeControl(panel); var msgBox = new XNAMessageBox(windowManager, Renderer.GetSafeString(caption, 1), Renderer.GetSafeString(description, 0), XNAMessageBoxButtons.YesNo); panel.AddChild(msgBox); msgBox.YesClickedAction = MsgBox_YesClicked; msgBox.NoClickedAction = MsgBox_NoClicked; return msgBox; } private static void MsgBox_NoClicked(XNAMessageBox messageBox) { var parent = (DarkeningPanel)messageBox.Parent; parent.Hide(); parent.Hidden += Parent_Hidden; } private static void MsgBox_YesClicked(XNAMessageBox messageBox) { var parent = (DarkeningPanel)messageBox.Parent; parent.Hide(); parent.Hidden += Parent_Hidden; } private static void Parent_Hidden(object sender, EventArgs e) { var darkeningPanel = (DarkeningPanel)sender; darkeningPanel.WindowManager.RemoveControl(darkeningPanel); darkeningPanel.Hidden -= Parent_Hidden; } #endregion } public enum XNAMessageBoxButtons { OK, YesNo, OKCancel } } ================================================ FILE: ClientGUI/XNAOptionsPanel.cs ================================================ using ClientCore; using ClientGUI.Settings; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System.Collections.Generic; namespace ClientGUI { /// /// A base class for all option panels. /// Handles custom game-specific panel options /// defined in INI files. /// public abstract class XNAOptionsPanel : XNAWindowBase { public XNAOptionsPanel(WindowManager windowManager, UserINISettings iniSettings) : base(windowManager) { IniSettings = iniSettings; } private readonly List userSettings = new List(); public override void Initialize() { ClientRectangle = new Rectangle(12, 47, Parent.Width - 24, Parent.Height - 94); BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; base.Initialize(); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; } private void GameProcessExited_Callback() { foreach (IUserSetting setting in userSettings) { if (!setting.ResetToDefaultOnGameExit) continue; if (setting is SettingCheckBoxBase cb) cb.Checked = cb.DefaultValue; else if (setting is SettingDropDownBase dd) dd.SelectedIndex = dd.DefaultValue; setting.Save(); } } /// /// Parses user-defined game options from an INI file. /// /// The INI file. public void ParseUserOptions(IniFile iniFile) { GetAttributes(iniFile); ParseExtraControls(iniFile, Name + "ExtraControls"); ReadChildControlAttributes(iniFile); } public override void AddChild(XNAControl child) { base.AddChild(child); if (child is IUserSetting setting) userSettings.Add(setting); } protected UserINISettings IniSettings { get; private set; } /// /// Saves the options of this panel. /// A bool that determines whether the /// client needs to restart for changes to apply. /// public virtual bool Save() { bool restartRequired = false; foreach (var setting in userSettings) restartRequired = setting.Save() || restartRequired; return restartRequired; } /// /// Refreshes the panel's settings to account for possible /// changes that could affect the functionality. /// /// A bool that determines whether the /// setting's value was changed. public virtual bool RefreshPanel() { bool valuesChanged = false; foreach (var setting in userSettings) { if (setting is IFileSetting fileSetting) valuesChanged = fileSetting.RefreshSetting() || valuesChanged; } return valuesChanged; } /// /// Loads the options of this panel. /// public virtual void Load() { foreach (var setting in userSettings) setting.Load(); } /// /// Enables or disables any options that should only be available when /// options window was opened in main menu. /// /// If true enables options, disables if false. public virtual void ToggleMainMenuOnlyOptions(bool enable) { } } } ================================================ FILE: ClientGUI/XNAPlayerSlotIndicator.cs ================================================ using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using ClientCore.Extensions; namespace ClientGUI { public enum PlayerSlotState { Empty, Unavailable, AI, NotReady, Ready, InGame, Warning, Error } public class XNAPlayerSlotIndicator : XNAIndicator { public static new Dictionary Textures { get; set; } public ToolTip ToolTip { get; private set; } public XNAPlayerSlotIndicator(WindowManager windowManager) : base(windowManager, Textures) { } public static void LoadTextures() { Textures = new Dictionary() { { PlayerSlotState.Empty, AssetLoader.LoadTextureUncached("statusEmpty.png") }, { PlayerSlotState.Unavailable, AssetLoader.LoadTextureUncached("statusUnavailable.png") }, { PlayerSlotState.AI, AssetLoader.LoadTextureUncached("statusAI.png") }, { PlayerSlotState.NotReady, AssetLoader.LoadTextureUncached("statusClear.png") }, { PlayerSlotState.Ready, AssetLoader.LoadTextureUncached("statusOk.png") }, { PlayerSlotState.InGame, AssetLoader.LoadTextureUncached("statusInProgress.png") }, { PlayerSlotState.Warning, AssetLoader.LoadTextureUncached("statusWarning.png") }, { PlayerSlotState.Error, AssetLoader.LoadTextureUncached("statusError.png") } }; } public override void Initialize() { base.Initialize(); ToolTip = new ToolTip(WindowManager, this); } public override void SwitchTexture(PlayerSlotState key) { base.SwitchTexture(key); switch (key) { case PlayerSlotState.Empty: ToolTip.Text = "The slot is empty.".L10N("Client:ClientGUI:SlotEmpty"); break; case PlayerSlotState.Unavailable: ToolTip.Text = "The slot is unavailable.".L10N("Client:ClientGUI:SlotUnavailable"); break; case PlayerSlotState.AI: ToolTip.Text = "The player is computer-controlled.".L10N("Client:ClientGUI:PlayerIsComputer"); break; case PlayerSlotState.NotReady: ToolTip.Text = "The player isn't ready.".L10N("Client:ClientGUI:PlayerIsNotReady"); break; case PlayerSlotState.Ready: ToolTip.Text = "The player is ready.".L10N("Client:ClientGUI:PlayerIsReady"); break; case PlayerSlotState.InGame: ToolTip.Text = "The player is in game.".L10N("Client:ClientGUI:PlayerIsInGame"); break; case PlayerSlotState.Warning: ToolTip.Text = "The player has some issue(s) that may impact gameplay.".L10N("Client:ClientGUI:PlayerHasIssue"); break; case PlayerSlotState.Error: ToolTip.Text = "There's a critical issue with the player.".L10N("Client:ClientGUI:PlayerHasCriticalIssue"); break; } } } } ================================================ FILE: ClientGUI/XNAWindow.cs ================================================ using ClientCore; using Rampastring.Tools; using System; using System.Collections.Generic; using Rampastring.XNAUI; namespace ClientGUI { /// /// A sub-window to be displayed inside the game window. /// Supports easy reading of child controls' attributes from an INI file. /// public class XNAWindow : XNAWindowBase { private const string GENERIC_WINDOW_INI = "GenericWindow.ini"; private const string GENERIC_WINDOW_SECTION = "GenericWindow"; private const string EXTRA_CONTROLS = "ExtraControls"; public XNAWindow(WindowManager windowManager) : base(windowManager) { } /// /// The INI file that was used for theming this window. /// protected IniFile ThemeIni { get; set; } public override float Alpha { get { return 1.0f; } } protected virtual void SetAttributesFromIni() { if (SafePath.GetFile(ProgramConstants.GetResourcePath(), FormattableString.Invariant($"{Name}.ini")).Exists) GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), FormattableString.Invariant($"{Name}.ini")))); else if (SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($"{Name}.ini")).Exists) GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), FormattableString.Invariant($"{Name}.ini")))); else if (SafePath.GetFile(ProgramConstants.GetResourcePath(), GENERIC_WINDOW_INI).Exists) GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetResourcePath(), GENERIC_WINDOW_INI))); else GetINIAttributes(new CCIniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), GENERIC_WINDOW_INI))); } /// /// Reads this window's attributes from an INI file. /// protected virtual void GetINIAttributes(IniFile iniFile) { ThemeIni = iniFile; List keys = iniFile.GetSectionKeys(Name); if (keys != null) { foreach (string key in keys) ParseINIAttribute(iniFile, key, iniFile.GetStringValue(Name, key, String.Empty)); } else { keys = iniFile.GetSectionKeys(GENERIC_WINDOW_SECTION); if (keys != null) { foreach (string key in keys) ParseINIAttribute(iniFile, key, iniFile.GetStringValue(GENERIC_WINDOW_SECTION, key, String.Empty)); } } ParseExtraControls(iniFile, EXTRA_CONTROLS); ReadChildControlAttributes(iniFile); } public override void Initialize() { base.Initialize(); SetAttributesFromIni(); } } } ================================================ FILE: ClientGUI/XNAWindowBase.cs ================================================ using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System.Linq; namespace ClientGUI { public class XNAWindowBase : XNAPanel { public XNAWindowBase(WindowManager windowManager) : base(windowManager) { PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.TILED; } /// /// Reads extra control information from a specific section of an INI file. /// /// The INI file. /// The section. protected virtual void ParseExtraControls(IniFile iniFile, string sectionName) { var section = iniFile.GetSection(sectionName); if (section == null) return; foreach (var kvp in section.Keys) { string[] parts = kvp.Value.Split(':'); if (parts.Length != 2) throw new ClientConfigurationException("Invalid ExtraControl specified in " + Name + ": " + kvp.Value); if (!Children.Any(child => child.Name == parts[0])) { XNAControl control = ClientGUICreator.GetXnaControl(parts[1]); control.Name = parts[0]; control.DrawOrder = -Children.Count; AddChild(control); } } } protected virtual void ReadChildControlAttributes(IniFile iniFile) { foreach (XNAControl child in Children) { if (!(typeof(XNAWindowBase).IsAssignableFrom(child.GetType()))) child.GetAttributes(iniFile); } } /// /// Creates a control with a given name, using the specified GUI creator /// and control type name. /// /// The to use. /// The name of the control's type. /// The name of the created control. /// The created control. protected virtual XNAControl CreateControl(GUICreator guiCreator, string controlTypeName, string controlName) { var control = guiCreator.CreateControl(WindowManager, controlTypeName); control.Name = controlName; control.DrawOrder = -Children.Count; AddChild(control); return control; } } } ================================================ FILE: ClientUpdater/ClientUpdater.csproj ================================================  CnCNet.ClientUpdater CnCNet Client Updater Library CnCNet.ClientUpdater ================================================ FILE: ClientUpdater/Compression/Common/CRC.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ // Common/CRC.cs namespace SevenZip { class CRC { public static readonly uint[] Table; static CRC() { Table = new uint[256]; const uint kPoly = 0xEDB88320; for (uint i = 0; i < 256; i++) { uint r = i; for (int j = 0; j < 8; j++) if ((r & 1) != 0) r = (r >> 1) ^ kPoly; else r >>= 1; Table[i] = r; } } uint _value = 0xFFFFFFFF; public void Init() { _value = 0xFFFFFFFF; } public void UpdateByte(byte b) { _value = Table[(((byte)(_value)) ^ b)] ^ (_value >> 8); } public void Update(byte[] data, uint offset, uint size) { for (uint i = 0; i < size; i++) _value = Table[(((byte)(_value)) ^ data[offset + i])] ^ (_value >> 8); } public uint GetDigest() { return _value ^ 0xFFFFFFFF; } static uint CalculateDigest(byte[] data, uint offset, uint size) { CRC crc = new CRC(); // crc.Init(); crc.Update(data, offset, size); return crc.GetDigest(); } static bool VerifyDigest(uint digest, byte[] data, uint offset, uint size) { return (CalculateDigest(data, offset, size) == digest); } } } ================================================ FILE: ClientUpdater/Compression/Common/CommandLineParser.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591 // CommandLineParser.cs using System; using System.Collections; namespace SevenZip.CommandLineParser { public enum SwitchType { Simple, PostMinus, LimitedPostString, UnLimitedPostString, PostChar } public class SwitchForm { public string IDString; public SwitchType Type; public bool Multi; public int MinLen; public int MaxLen; public string PostCharSet; public SwitchForm(string idString, SwitchType type, bool multi, int minLen, int maxLen, string postCharSet) { IDString = idString; Type = type; Multi = multi; MinLen = minLen; MaxLen = maxLen; PostCharSet = postCharSet; } public SwitchForm(string idString, SwitchType type, bool multi, int minLen): this(idString, type, multi, minLen, 0, "") { } public SwitchForm(string idString, SwitchType type, bool multi): this(idString, type, multi, 0) { } } public class SwitchResult { public bool ThereIs; public bool WithMinus; public ArrayList PostStrings = new ArrayList(); public int PostCharIndex; public SwitchResult() { ThereIs = false; } } public class Parser { public ArrayList NonSwitchStrings = new ArrayList(); SwitchResult[] _switches; public Parser(int numSwitches) { _switches = new SwitchResult[numSwitches]; for (int i = 0; i < numSwitches; i++) _switches[i] = new SwitchResult(); } bool ParseString(string srcString, SwitchForm[] switchForms) { int len = srcString.Length; if (len == 0) return false; int pos = 0; if (!IsItSwitchChar(srcString[pos])) return false; while (pos < len) { if (IsItSwitchChar(srcString[pos])) pos++; const int kNoLen = -1; int matchedSwitchIndex = 0; int maxLen = kNoLen; for (int switchIndex = 0; switchIndex < _switches.Length; switchIndex++) { int switchLen = switchForms[switchIndex].IDString.Length; if (switchLen <= maxLen || pos + switchLen > len) continue; if (String.Compare(switchForms[switchIndex].IDString, 0, srcString, pos, switchLen, true) == 0) { matchedSwitchIndex = switchIndex; maxLen = switchLen; } } if (maxLen == kNoLen) throw new Exception("maxLen == kNoLen"); SwitchResult matchedSwitch = _switches[matchedSwitchIndex]; SwitchForm switchForm = switchForms[matchedSwitchIndex]; if ((!switchForm.Multi) && matchedSwitch.ThereIs) throw new Exception("switch must be single"); matchedSwitch.ThereIs = true; pos += maxLen; int tailSize = len - pos; SwitchType type = switchForm.Type; switch (type) { case SwitchType.PostMinus: { if (tailSize == 0) matchedSwitch.WithMinus = false; else { matchedSwitch.WithMinus = (srcString[pos] == kSwitchMinus); if (matchedSwitch.WithMinus) pos++; } break; } case SwitchType.PostChar: { if (tailSize < switchForm.MinLen) throw new Exception("switch is not full"); string charSet = switchForm.PostCharSet; const int kEmptyCharValue = -1; if (tailSize == 0) matchedSwitch.PostCharIndex = kEmptyCharValue; else { int index = charSet.IndexOf(srcString[pos]); if (index < 0) matchedSwitch.PostCharIndex = kEmptyCharValue; else { matchedSwitch.PostCharIndex = index; pos++; } } break; } case SwitchType.LimitedPostString: case SwitchType.UnLimitedPostString: { int minLen = switchForm.MinLen; if (tailSize < minLen) throw new Exception("switch is not full"); if (type == SwitchType.UnLimitedPostString) { matchedSwitch.PostStrings.Add(srcString.Substring(pos)); return true; } String stringSwitch = srcString.Substring(pos, minLen); pos += minLen; for (int i = minLen; i < switchForm.MaxLen && pos < len; i++, pos++) { char c = srcString[pos]; if (IsItSwitchChar(c)) break; stringSwitch += c; } matchedSwitch.PostStrings.Add(stringSwitch); break; } } } return true; } public void ParseStrings(SwitchForm[] switchForms, string[] commandStrings) { int numCommandStrings = commandStrings.Length; bool stopSwitch = false; for (int i = 0; i < numCommandStrings; i++) { string s = commandStrings[i]; if (stopSwitch) NonSwitchStrings.Add(s); else if (s == kStopSwitchParsing) stopSwitch = true; else if (!ParseString(s, switchForms)) NonSwitchStrings.Add(s); } } public SwitchResult this[int index] { get { return _switches[index]; } } public static int ParseCommand(CommandForm[] commandForms, string commandString, out string postString) { for (int i = 0; i < commandForms.Length; i++) { string id = commandForms[i].IDString; if (commandForms[i].PostStringMode) { if (commandString.IndexOf(id) == 0) { postString = commandString.Substring(id.Length); return i; } } else if (commandString == id) { postString = ""; return i; } } postString = ""; return -1; } static bool ParseSubCharsCommand(int numForms, CommandSubCharsSet[] forms, string commandString, ArrayList indices) { indices.Clear(); int numUsedChars = 0; for (int i = 0; i < numForms; i++) { CommandSubCharsSet charsSet = forms[i]; int currentIndex = -1; int len = charsSet.Chars.Length; for (int j = 0; j < len; j++) { char c = charsSet.Chars[j]; int newIndex = commandString.IndexOf(c); if (newIndex >= 0) { if (currentIndex >= 0) return false; if (commandString.IndexOf(c, newIndex + 1) >= 0) return false; currentIndex = j; numUsedChars++; } } if (currentIndex == -1 && !charsSet.EmptyAllowed) return false; indices.Add(currentIndex); } return (numUsedChars == commandString.Length); } const char kSwitchID1 = '-'; const char kSwitchID2 = '/'; const char kSwitchMinus = '-'; const string kStopSwitchParsing = "--"; static bool IsItSwitchChar(char c) { return (c == kSwitchID1 || c == kSwitchID2); } } public class CommandForm { public string IDString = ""; public bool PostStringMode = false; public CommandForm(string idString, bool postStringMode) { IDString = idString; PostStringMode = postStringMode; } } class CommandSubCharsSet { public string Chars = ""; public bool EmptyAllowed = false; } } ================================================ FILE: ClientUpdater/Compression/Common/InBuffer.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591 // InBuffer.cs namespace SevenZip.Buffer { public class InBuffer { byte[] m_Buffer; uint m_Pos; uint m_Limit; uint m_BufferSize; System.IO.Stream m_Stream; bool m_StreamWasExhausted; ulong m_ProcessedSize; public InBuffer(uint bufferSize) { m_Buffer = new byte[bufferSize]; m_BufferSize = bufferSize; } public void Init(System.IO.Stream stream) { m_Stream = stream; m_ProcessedSize = 0; m_Limit = 0; m_Pos = 0; m_StreamWasExhausted = false; } public bool ReadBlock() { if (m_StreamWasExhausted) return false; m_ProcessedSize += m_Pos; int aNumProcessedBytes = m_Stream.Read(m_Buffer, 0, (int)m_BufferSize); m_Pos = 0; m_Limit = (uint)aNumProcessedBytes; m_StreamWasExhausted = (aNumProcessedBytes == 0); return (!m_StreamWasExhausted); } public void ReleaseStream() { // m_Stream.Close(); m_Stream = null; } public bool ReadByte(byte b) // check it { if (m_Pos >= m_Limit) if (!ReadBlock()) return false; b = m_Buffer[m_Pos++]; return true; } public byte ReadByte() { // return (byte)m_Stream.ReadByte(); if (m_Pos >= m_Limit) if (!ReadBlock()) return 0xFF; return m_Buffer[m_Pos++]; } public ulong GetProcessedSize() { return m_ProcessedSize + m_Pos; } } } ================================================ FILE: ClientUpdater/Compression/Common/OutBuffer.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591 // OutBuffer.cs namespace SevenZip.Buffer { public class OutBuffer { byte[] m_Buffer; uint m_Pos; uint m_BufferSize; System.IO.Stream m_Stream; ulong m_ProcessedSize; public OutBuffer(uint bufferSize) { m_Buffer = new byte[bufferSize]; m_BufferSize = bufferSize; } public void SetStream(System.IO.Stream stream) { m_Stream = stream; } public void FlushStream() { m_Stream.Flush(); } public void CloseStream() { m_Stream.Close(); } public void ReleaseStream() { m_Stream = null; } public void Init() { m_ProcessedSize = 0; m_Pos = 0; } public void WriteByte(byte b) { m_Buffer[m_Pos++] = b; if (m_Pos >= m_BufferSize) FlushData(); } public void FlushData() { if (m_Pos == 0) return; m_Stream.Write(m_Buffer, 0, (int)m_Pos); m_Pos = 0; } public ulong GetProcessedSize() { return m_ProcessedSize + m_Pos; } } } ================================================ FILE: ClientUpdater/Compression/CompressionHelper.cs ================================================ // Copyright 2022-2024 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . namespace ClientUpdater.Compression; using System; using System.Buffers.Binary; using System.IO; using System.Threading; using System.Threading.Tasks; using SevenZip.Compression.LZMA; /// /// LZMA compression helper. /// public static class CompressionHelper { /// /// Compress file using LZMA. /// /// Input file path. /// Output file path. public static async ValueTask CompressFileAsync(string inputFilename, string outputFilename, CancellationToken cancellationToken = default) { var encoder = new Encoder(cancellationToken); var inputStream = new FileStream(inputFilename, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan); using (inputStream) { var outputStream = new FileStream(outputFilename, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (outputStream) { encoder.WriteCoderProperties(outputStream); byte[] lengthBytes = new byte[sizeof(long)]; BinaryPrimitives.WriteInt64LittleEndian(lengthBytes, inputStream.Length); await outputStream.WriteAsync(lengthBytes.AsMemory(0, sizeof(long)), cancellationToken).ConfigureAwait(false); encoder.Code(inputStream, outputStream, inputStream.Length, outputStream.Length, null); } } } /// /// Decompress file using LZMA. /// /// Input file path. /// Output file path. public static async ValueTask DecompressFileAsync(string inputFilename, string outputFilename, CancellationToken cancellationToken = default) { var decoder = new Decoder(cancellationToken); var inputStream = new FileStream(inputFilename, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan); using (inputStream) { var outputStream = new FileStream(outputFilename, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (outputStream) { byte[] properties = new byte[5]; byte[] fileLengthArray = new byte[sizeof(long)]; await inputStream.ReadAsync(properties, cancellationToken).ConfigureAwait(false); await inputStream.ReadAsync(fileLengthArray, cancellationToken).ConfigureAwait(false); long fileLength = BinaryPrimitives.ReadInt64LittleEndian(fileLengthArray); decoder.SetDecoderProperties(properties); decoder.Code(inputStream, outputStream, inputStream.Length, fileLength, null); } } } } ================================================ FILE: ClientUpdater/Compression/ICoder.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1570, CS1591 // ICoder.h using System; namespace SevenZip { /// /// The exception that is thrown when an error in input stream occurs during decoding. /// class DataErrorException : ApplicationException { public DataErrorException(): base("Data Error") { } } /// /// The exception that is thrown when the value of an argument is outside the allowable range. /// class InvalidParamException : ApplicationException { public InvalidParamException(): base("Invalid Parameter") { } } public interface ICodeProgress { /// /// Callback progress. /// /// /// input size. -1 if unknown. /// /// /// output size. -1 if unknown. /// void SetProgress(Int64 inSize, Int64 outSize); }; public interface ICoder { /// /// Codes streams. /// /// /// input Stream. /// /// /// output Stream. /// /// /// input Size. -1 if unknown. /// /// /// output Size. -1 if unknown. /// /// /// callback progress reference. /// /// /// if input stream is not valid /// void Code(System.IO.Stream inStream, System.IO.Stream outStream, Int64 inSize, Int64 outSize, ICodeProgress progress); }; /* public interface ICoder2 { void Code(ISequentialInStream []inStreams, const UInt64 []inSizes, ISequentialOutStream []outStreams, UInt64 []outSizes, ICodeProgress progress); }; */ /// /// Provides the fields that represent properties idenitifiers for compressing. /// public enum CoderPropID { /// /// Specifies default property. /// DefaultProp = 0, /// /// Specifies size of dictionary. /// DictionarySize, /// /// Specifies size of memory for PPM*. /// UsedMemorySize, /// /// Specifies order for PPM methods. /// Order, /// /// Specifies Block Size. /// BlockSize, /// /// Specifies number of postion state bits for LZMA (0 <= x <= 4). /// PosStateBits, /// /// Specifies number of literal context bits for LZMA (0 <= x <= 8). /// LitContextBits, /// /// Specifies number of literal position bits for LZMA (0 <= x <= 4). /// LitPosBits, /// /// Specifies number of fast bytes for LZ*. /// NumFastBytes, /// /// Specifies match finder. LZMA: "BT2", "BT4" or "BT4B". /// MatchFinder, /// /// Specifies the number of match finder cyckes. /// MatchFinderCycles, /// /// Specifies number of passes. /// NumPasses, /// /// Specifies number of algorithm. /// Algorithm, /// /// Specifies the number of threads. /// NumThreads, /// /// Specifies mode with end marker. /// EndMarker }; public interface ISetCoderProperties { void SetCoderProperties(CoderPropID[] propIDs, object[] properties); }; public interface IWriteCoderProperties { void WriteCoderProperties(System.IO.Stream outStream); } public interface ISetDecoderProperties { void SetDecoderProperties(byte[] properties); } } ================================================ FILE: ClientUpdater/Compression/LZ/IMatchFinder.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ // IMatchFinder.cs using System; namespace SevenZip.Compression.LZ { interface IInWindowStream { void SetStream(System.IO.Stream inStream); void Init(); void ReleaseStream(); Byte GetIndexByte(Int32 index); UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit); UInt32 GetNumAvailableBytes(); } interface IMatchFinder : IInWindowStream { void Create(UInt32 historySize, UInt32 keepAddBufferBefore, UInt32 matchMaxLen, UInt32 keepAddBufferAfter); UInt32 GetMatches(UInt32[] distances); void Skip(UInt32 num); } } ================================================ FILE: ClientUpdater/Compression/LZ/LzBinTree.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591 // LzBinTree.cs using System; namespace SevenZip.Compression.LZ { public class BinTree : InWindow, IMatchFinder { UInt32 _cyclicBufferPos; UInt32 _cyclicBufferSize = 0; UInt32 _matchMaxLen; UInt32[] _son; UInt32[] _hash; UInt32 _cutValue = 0xFF; UInt32 _hashMask; UInt32 _hashSizeSum = 0; bool HASH_ARRAY = true; const UInt32 kHash2Size = 1 << 10; const UInt32 kHash3Size = 1 << 16; const UInt32 kBT2HashSize = 1 << 16; const UInt32 kStartMaxLen = 1; const UInt32 kHash3Offset = kHash2Size; const UInt32 kEmptyHashValue = 0; const UInt32 kMaxValForNormalize = ((UInt32)1 << 31) - 1; UInt32 kNumHashDirectBytes = 0; UInt32 kMinMatchCheck = 4; UInt32 kFixHashSize = kHash2Size + kHash3Size; public void SetType(int numHashBytes) { HASH_ARRAY = (numHashBytes > 2); if (HASH_ARRAY) { kNumHashDirectBytes = 0; kMinMatchCheck = 4; kFixHashSize = kHash2Size + kHash3Size; } else { kNumHashDirectBytes = 2; kMinMatchCheck = 2 + 1; kFixHashSize = 0; } } public new void SetStream(System.IO.Stream stream) { base.SetStream(stream); } public new void ReleaseStream() { base.ReleaseStream(); } public new void Init() { base.Init(); for (UInt32 i = 0; i < _hashSizeSum; i++) _hash[i] = kEmptyHashValue; _cyclicBufferPos = 0; ReduceOffsets(-1); } public new void MovePos() { if (++_cyclicBufferPos >= _cyclicBufferSize) _cyclicBufferPos = 0; base.MovePos(); if (_pos == kMaxValForNormalize) Normalize(); } public new Byte GetIndexByte(Int32 index) { return base.GetIndexByte(index); } public new UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit) { return base.GetMatchLen(index, distance, limit); } public new UInt32 GetNumAvailableBytes() { return base.GetNumAvailableBytes(); } public void Create(UInt32 historySize, UInt32 keepAddBufferBefore, UInt32 matchMaxLen, UInt32 keepAddBufferAfter) { if (historySize > kMaxValForNormalize - 256) throw new Exception(); _cutValue = 16 + (matchMaxLen >> 1); UInt32 windowReservSize = (historySize + keepAddBufferBefore + matchMaxLen + keepAddBufferAfter) / 2 + 256; base.Create(historySize + keepAddBufferBefore, matchMaxLen + keepAddBufferAfter, windowReservSize); _matchMaxLen = matchMaxLen; UInt32 cyclicBufferSize = historySize + 1; if (_cyclicBufferSize != cyclicBufferSize) _son = new UInt32[(_cyclicBufferSize = cyclicBufferSize) * 2]; UInt32 hs = kBT2HashSize; if (HASH_ARRAY) { hs = historySize - 1; hs |= (hs >> 1); hs |= (hs >> 2); hs |= (hs >> 4); hs |= (hs >> 8); hs >>= 1; hs |= 0xFFFF; if (hs > (1 << 24)) hs >>= 1; _hashMask = hs; hs++; hs += kFixHashSize; } if (hs != _hashSizeSum) _hash = new UInt32[_hashSizeSum = hs]; } public UInt32 GetMatches(UInt32[] distances) { UInt32 lenLimit; if (_pos + _matchMaxLen <= _streamPos) lenLimit = _matchMaxLen; else { lenLimit = _streamPos - _pos; if (lenLimit < kMinMatchCheck) { MovePos(); return 0; } } UInt32 offset = 0; UInt32 matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0; UInt32 cur = _bufferOffset + _pos; UInt32 maxLen = kStartMaxLen; // to avoid items for len < hashSize; UInt32 hashValue, hash2Value = 0, hash3Value = 0; if (HASH_ARRAY) { UInt32 temp = CRC.Table[_bufferBase[cur]] ^ _bufferBase[cur + 1]; hash2Value = temp & (kHash2Size - 1); temp ^= ((UInt32)(_bufferBase[cur + 2]) << 8); hash3Value = temp & (kHash3Size - 1); hashValue = (temp ^ (CRC.Table[_bufferBase[cur + 3]] << 5)) & _hashMask; } else hashValue = _bufferBase[cur] ^ ((UInt32)(_bufferBase[cur + 1]) << 8); UInt32 curMatch = _hash[kFixHashSize + hashValue]; if (HASH_ARRAY) { UInt32 curMatch2 = _hash[hash2Value]; UInt32 curMatch3 = _hash[kHash3Offset + hash3Value]; _hash[hash2Value] = _pos; _hash[kHash3Offset + hash3Value] = _pos; if (curMatch2 > matchMinPos) if (_bufferBase[_bufferOffset + curMatch2] == _bufferBase[cur]) { distances[offset++] = maxLen = 2; distances[offset++] = _pos - curMatch2 - 1; } if (curMatch3 > matchMinPos) if (_bufferBase[_bufferOffset + curMatch3] == _bufferBase[cur]) { if (curMatch3 == curMatch2) offset -= 2; distances[offset++] = maxLen = 3; distances[offset++] = _pos - curMatch3 - 1; curMatch2 = curMatch3; } if (offset != 0 && curMatch2 == curMatch) { offset -= 2; maxLen = kStartMaxLen; } } _hash[kFixHashSize + hashValue] = _pos; UInt32 ptr0 = (_cyclicBufferPos << 1) + 1; UInt32 ptr1 = (_cyclicBufferPos << 1); UInt32 len0, len1; len0 = len1 = kNumHashDirectBytes; if (kNumHashDirectBytes != 0) { if (curMatch > matchMinPos) { if (_bufferBase[_bufferOffset + curMatch + kNumHashDirectBytes] != _bufferBase[cur + kNumHashDirectBytes]) { distances[offset++] = maxLen = kNumHashDirectBytes; distances[offset++] = _pos - curMatch - 1; } } } UInt32 count = _cutValue; while(true) { if(curMatch <= matchMinPos || count-- == 0) { _son[ptr0] = _son[ptr1] = kEmptyHashValue; break; } UInt32 delta = _pos - curMatch; UInt32 cyclicPos = ((delta <= _cyclicBufferPos) ? (_cyclicBufferPos - delta) : (_cyclicBufferPos - delta + _cyclicBufferSize)) << 1; UInt32 pby1 = _bufferOffset + curMatch; UInt32 len = Math.Min(len0, len1); if (_bufferBase[pby1 + len] == _bufferBase[cur + len]) { while(++len != lenLimit) if (_bufferBase[pby1 + len] != _bufferBase[cur + len]) break; if (maxLen < len) { distances[offset++] = maxLen = len; distances[offset++] = delta - 1; if (len == lenLimit) { _son[ptr1] = _son[cyclicPos]; _son[ptr0] = _son[cyclicPos + 1]; break; } } } if (_bufferBase[pby1 + len] < _bufferBase[cur + len]) { _son[ptr1] = curMatch; ptr1 = cyclicPos + 1; curMatch = _son[ptr1]; len1 = len; } else { _son[ptr0] = curMatch; ptr0 = cyclicPos; curMatch = _son[ptr0]; len0 = len; } } MovePos(); return offset; } public void Skip(UInt32 num) { do { UInt32 lenLimit; if (_pos + _matchMaxLen <= _streamPos) lenLimit = _matchMaxLen; else { lenLimit = _streamPos - _pos; if (lenLimit < kMinMatchCheck) { MovePos(); continue; } } UInt32 matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0; UInt32 cur = _bufferOffset + _pos; UInt32 hashValue; if (HASH_ARRAY) { UInt32 temp = CRC.Table[_bufferBase[cur]] ^ _bufferBase[cur + 1]; UInt32 hash2Value = temp & (kHash2Size - 1); _hash[hash2Value] = _pos; temp ^= ((UInt32)(_bufferBase[cur + 2]) << 8); UInt32 hash3Value = temp & (kHash3Size - 1); _hash[kHash3Offset + hash3Value] = _pos; hashValue = (temp ^ (CRC.Table[_bufferBase[cur + 3]] << 5)) & _hashMask; } else hashValue = _bufferBase[cur] ^ ((UInt32)(_bufferBase[cur + 1]) << 8); UInt32 curMatch = _hash[kFixHashSize + hashValue]; _hash[kFixHashSize + hashValue] = _pos; UInt32 ptr0 = (_cyclicBufferPos << 1) + 1; UInt32 ptr1 = (_cyclicBufferPos << 1); UInt32 len0, len1; len0 = len1 = kNumHashDirectBytes; UInt32 count = _cutValue; while (true) { if (curMatch <= matchMinPos || count-- == 0) { _son[ptr0] = _son[ptr1] = kEmptyHashValue; break; } UInt32 delta = _pos - curMatch; UInt32 cyclicPos = ((delta <= _cyclicBufferPos) ? (_cyclicBufferPos - delta) : (_cyclicBufferPos - delta + _cyclicBufferSize)) << 1; UInt32 pby1 = _bufferOffset + curMatch; UInt32 len = Math.Min(len0, len1); if (_bufferBase[pby1 + len] == _bufferBase[cur + len]) { while (++len != lenLimit) if (_bufferBase[pby1 + len] != _bufferBase[cur + len]) break; if (len == lenLimit) { _son[ptr1] = _son[cyclicPos]; _son[ptr0] = _son[cyclicPos + 1]; break; } } if (_bufferBase[pby1 + len] < _bufferBase[cur + len]) { _son[ptr1] = curMatch; ptr1 = cyclicPos + 1; curMatch = _son[ptr1]; len1 = len; } else { _son[ptr0] = curMatch; ptr0 = cyclicPos; curMatch = _son[ptr0]; len0 = len; } } MovePos(); } while (--num != 0); } void NormalizeLinks(UInt32[] items, UInt32 numItems, UInt32 subValue) { for (UInt32 i = 0; i < numItems; i++) { UInt32 value = items[i]; if (value <= subValue) value = kEmptyHashValue; else value -= subValue; items[i] = value; } } void Normalize() { UInt32 subValue = _pos - _cyclicBufferSize; NormalizeLinks(_son, _cyclicBufferSize * 2, subValue); NormalizeLinks(_hash, _hashSizeSum, subValue); ReduceOffsets((Int32)subValue); } public void SetCutValue(UInt32 cutValue) { _cutValue = cutValue; } } } ================================================ FILE: ClientUpdater/Compression/LZ/LzInWindow.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591 // LzInWindow.cs using System; namespace SevenZip.Compression.LZ { public class InWindow { public Byte[] _bufferBase = null; // pointer to buffer with data System.IO.Stream _stream; UInt32 _posLimit; // offset (from _buffer) of first byte when new block reading must be done bool _streamEndWasReached; // if (true) then _streamPos shows real end of stream UInt32 _pointerToLastSafePosition; public UInt32 _bufferOffset; public UInt32 _blockSize; // Size of Allocated memory block public UInt32 _pos; // offset (from _buffer) of curent byte UInt32 _keepSizeBefore; // how many BYTEs must be kept in buffer before _pos UInt32 _keepSizeAfter; // how many BYTEs must be kept buffer after _pos public UInt32 _streamPos; // offset (from _buffer) of first not read byte from Stream public void MoveBlock() { UInt32 offset = (UInt32)(_bufferOffset) + _pos - _keepSizeBefore; // we need one additional byte, since MovePos moves on 1 byte. if (offset > 0) offset--; UInt32 numBytes = (UInt32)(_bufferOffset) + _streamPos - offset; // check negative offset ???? for (UInt32 i = 0; i < numBytes; i++) _bufferBase[i] = _bufferBase[offset + i]; _bufferOffset -= offset; } public virtual void ReadBlock() { if (_streamEndWasReached) return; while (true) { int size = (int)((0 - _bufferOffset) + _blockSize - _streamPos); if (size == 0) return; int numReadBytes = _stream.Read(_bufferBase, (int)(_bufferOffset + _streamPos), size); if (numReadBytes == 0) { _posLimit = _streamPos; UInt32 pointerToPostion = _bufferOffset + _posLimit; if (pointerToPostion > _pointerToLastSafePosition) _posLimit = (UInt32)(_pointerToLastSafePosition - _bufferOffset); _streamEndWasReached = true; return; } _streamPos += (UInt32)numReadBytes; if (_streamPos >= _pos + _keepSizeAfter) _posLimit = _streamPos - _keepSizeAfter; } } void Free() { _bufferBase = null; } public void Create(UInt32 keepSizeBefore, UInt32 keepSizeAfter, UInt32 keepSizeReserv) { _keepSizeBefore = keepSizeBefore; _keepSizeAfter = keepSizeAfter; UInt32 blockSize = keepSizeBefore + keepSizeAfter + keepSizeReserv; if (_bufferBase == null || _blockSize != blockSize) { Free(); _blockSize = blockSize; _bufferBase = new Byte[_blockSize]; } _pointerToLastSafePosition = _blockSize - keepSizeAfter; } public void SetStream(System.IO.Stream stream) { _stream = stream; } public void ReleaseStream() { _stream = null; } public void Init() { _bufferOffset = 0; _pos = 0; _streamPos = 0; _streamEndWasReached = false; ReadBlock(); } public void MovePos() { _pos++; if (_pos > _posLimit) { UInt32 pointerToPostion = _bufferOffset + _pos; if (pointerToPostion > _pointerToLastSafePosition) MoveBlock(); ReadBlock(); } } public Byte GetIndexByte(Int32 index) { return _bufferBase[_bufferOffset + _pos + index]; } // index + limit have not to exceed _keepSizeAfter; public UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit) { if (_streamEndWasReached) if ((_pos + index) + limit > _streamPos) limit = _streamPos - (UInt32)(_pos + index); distance++; // Byte *pby = _buffer + (size_t)_pos + index; UInt32 pby = _bufferOffset + _pos + (UInt32)index; UInt32 i; for (i = 0; i < limit && _bufferBase[pby + i] == _bufferBase[pby + i - distance]; i++); return i; } public UInt32 GetNumAvailableBytes() { return _streamPos - _pos; } public void ReduceOffsets(Int32 subValue) { _bufferOffset += (UInt32)subValue; _posLimit -= (UInt32)subValue; _pos -= (UInt32)subValue; _streamPos -= (UInt32)subValue; } } } ================================================ FILE: ClientUpdater/Compression/LZ/LzOutWindow.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591 // LzOutWindow.cs namespace SevenZip.Compression.LZ { public class OutWindow { byte[] _buffer = null; uint _pos; uint _windowSize = 0; uint _streamPos; System.IO.Stream _stream; public uint TrainSize = 0; public void Create(uint windowSize) { if (_windowSize != windowSize) { // System.GC.Collect(); _buffer = new byte[windowSize]; } _windowSize = windowSize; _pos = 0; _streamPos = 0; } public void Init(System.IO.Stream stream, bool solid) { ReleaseStream(); _stream = stream; if (!solid) { _streamPos = 0; _pos = 0; TrainSize = 0; } } public bool Train(System.IO.Stream stream) { long len = stream.Length; uint size = (len < _windowSize) ? (uint)len : _windowSize; TrainSize = size; stream.Position = len - size; _streamPos = _pos = 0; while (size > 0) { uint curSize = _windowSize - _pos; if (size < curSize) curSize = size; int numReadBytes = stream.Read(_buffer, (int)_pos, (int)curSize); if (numReadBytes == 0) return false; size -= (uint)numReadBytes; _pos += (uint)numReadBytes; _streamPos += (uint)numReadBytes; if (_pos == _windowSize) _streamPos = _pos = 0; } return true; } public void ReleaseStream() { Flush(); _stream = null; } public void Flush() { uint size = _pos - _streamPos; if (size == 0) return; _stream.Write(_buffer, (int)_streamPos, (int)size); if (_pos >= _windowSize) _pos = 0; _streamPos = _pos; } public void CopyBlock(uint distance, uint len) { uint pos = _pos - distance - 1; if (pos >= _windowSize) pos += _windowSize; for (; len > 0; len--) { if (pos >= _windowSize) pos = 0; _buffer[_pos++] = _buffer[pos++]; if (_pos >= _windowSize) Flush(); } } public void PutByte(byte b) { _buffer[_pos++] = b; if (_pos >= _windowSize) Flush(); } public byte GetByte(uint distance) { uint pos = _pos - distance - 1; if (pos >= _windowSize) pos += _windowSize; return _buffer[pos]; } } } ================================================ FILE: ClientUpdater/Compression/LZMA/LzmaBase.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ // LzmaBase.cs namespace SevenZip.Compression.LZMA { internal abstract class Base { public const uint kNumRepDistances = 4; public const uint kNumStates = 12; // static byte []kLiteralNextStates = {0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 4, 5}; // static byte []kMatchNextStates = {7, 7, 7, 7, 7, 7, 7, 10, 10, 10, 10, 10}; // static byte []kRepNextStates = {8, 8, 8, 8, 8, 8, 8, 11, 11, 11, 11, 11}; // static byte []kShortRepNextStates = {9, 9, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11}; public struct State { public uint Index; public void Init() { Index = 0; } public void UpdateChar() { if (Index < 4) Index = 0; else if (Index < 10) Index -= 3; else Index -= 6; } public void UpdateMatch() { Index = (uint)(Index < 7 ? 7 : 10); } public void UpdateRep() { Index = (uint)(Index < 7 ? 8 : 11); } public void UpdateShortRep() { Index = (uint)(Index < 7 ? 9 : 11); } public bool IsCharState() { return Index < 7; } } public const int kNumPosSlotBits = 6; public const int kDicLogSizeMin = 0; // public const int kDicLogSizeMax = 30; // public const uint kDistTableSizeMax = kDicLogSizeMax * 2; public const int kNumLenToPosStatesBits = 2; // it's for speed optimization public const uint kNumLenToPosStates = 1 << kNumLenToPosStatesBits; public const uint kMatchMinLen = 2; public static uint GetLenToPosState(uint len) { len -= kMatchMinLen; if (len < kNumLenToPosStates) return len; return (uint)(kNumLenToPosStates - 1); } public const int kNumAlignBits = 4; public const uint kAlignTableSize = 1 << kNumAlignBits; public const uint kAlignMask = (kAlignTableSize - 1); public const uint kStartPosModelIndex = 4; public const uint kEndPosModelIndex = 14; public const uint kNumPosModels = kEndPosModelIndex - kStartPosModelIndex; public const uint kNumFullDistances = 1 << ((int)kEndPosModelIndex / 2); public const uint kNumLitPosStatesBitsEncodingMax = 4; public const uint kNumLitContextBitsMax = 8; public const int kNumPosStatesBitsMax = 4; public const uint kNumPosStatesMax = (1 << kNumPosStatesBitsMax); public const int kNumPosStatesBitsEncodingMax = 4; public const uint kNumPosStatesEncodingMax = (1 << kNumPosStatesBitsEncodingMax); public const int kNumLowLenBits = 3; public const int kNumMidLenBits = 3; public const int kNumHighLenBits = 8; public const uint kNumLowLenSymbols = 1 << kNumLowLenBits; public const uint kNumMidLenSymbols = 1 << kNumMidLenBits; public const uint kNumLenSymbols = kNumLowLenSymbols + kNumMidLenSymbols + (1 << kNumHighLenBits); public const uint kMatchMaxLen = kMatchMinLen + kNumLenSymbols - 1; } } ================================================ FILE: ClientUpdater/Compression/LZMA/LzmaDecoder.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591, CS8073 // LzmaDecoder.cs using System; namespace SevenZip.Compression.LZMA { using System.Threading; using RangeCoder; public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream { class LenDecoder { BitDecoder m_Choice = new BitDecoder(); BitDecoder m_Choice2 = new BitDecoder(); BitTreeDecoder[] m_LowCoder = new BitTreeDecoder[Base.kNumPosStatesMax]; BitTreeDecoder[] m_MidCoder = new BitTreeDecoder[Base.kNumPosStatesMax]; BitTreeDecoder m_HighCoder = new BitTreeDecoder(Base.kNumHighLenBits); uint m_NumPosStates = 0; public void Create(uint numPosStates) { for (uint posState = m_NumPosStates; posState < numPosStates; posState++) { m_LowCoder[posState] = new BitTreeDecoder(Base.kNumLowLenBits); m_MidCoder[posState] = new BitTreeDecoder(Base.kNumMidLenBits); } m_NumPosStates = numPosStates; } public void Init() { m_Choice.Init(); for (uint posState = 0; posState < m_NumPosStates; posState++) { m_LowCoder[posState].Init(); m_MidCoder[posState].Init(); } m_Choice2.Init(); m_HighCoder.Init(); } public uint Decode(RangeCoder.Decoder rangeDecoder, uint posState) { if (m_Choice.Decode(rangeDecoder) == 0) return m_LowCoder[posState].Decode(rangeDecoder); else { uint symbol = Base.kNumLowLenSymbols; if (m_Choice2.Decode(rangeDecoder) == 0) symbol += m_MidCoder[posState].Decode(rangeDecoder); else { symbol += Base.kNumMidLenSymbols; symbol += m_HighCoder.Decode(rangeDecoder); } return symbol; } } } class LiteralDecoder { struct Decoder2 { BitDecoder[] m_Decoders; public void Create() { m_Decoders = new BitDecoder[0x300]; } public void Init() { for (int i = 0; i < 0x300; i++) m_Decoders[i].Init(); } public byte DecodeNormal(RangeCoder.Decoder rangeDecoder) { uint symbol = 1; do symbol = (symbol << 1) | m_Decoders[symbol].Decode(rangeDecoder); while (symbol < 0x100); return (byte)symbol; } public byte DecodeWithMatchByte(RangeCoder.Decoder rangeDecoder, byte matchByte) { uint symbol = 1; do { uint matchBit = (uint)(matchByte >> 7) & 1; matchByte <<= 1; uint bit = m_Decoders[((1 + matchBit) << 8) + symbol].Decode(rangeDecoder); symbol = (symbol << 1) | bit; if (matchBit != bit) { while (symbol < 0x100) symbol = (symbol << 1) | m_Decoders[symbol].Decode(rangeDecoder); break; } } while (symbol < 0x100); return (byte)symbol; } } Decoder2[] m_Coders; int m_NumPrevBits; int m_NumPosBits; uint m_PosMask; public void Create(int numPosBits, int numPrevBits) { if (m_Coders != null && m_NumPrevBits == numPrevBits && m_NumPosBits == numPosBits) return; m_NumPosBits = numPosBits; m_PosMask = ((uint)1 << numPosBits) - 1; m_NumPrevBits = numPrevBits; uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); m_Coders = new Decoder2[numStates]; for (uint i = 0; i < numStates; i++) m_Coders[i].Create(); } public void Init() { uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); for (uint i = 0; i < numStates; i++) m_Coders[i].Init(); } uint GetState(uint pos, byte prevByte) { return ((pos & m_PosMask) << m_NumPrevBits) + (uint)(prevByte >> (8 - m_NumPrevBits)); } public byte DecodeNormal(RangeCoder.Decoder rangeDecoder, uint pos, byte prevByte) { return m_Coders[GetState(pos, prevByte)].DecodeNormal(rangeDecoder); } public byte DecodeWithMatchByte(RangeCoder.Decoder rangeDecoder, uint pos, byte prevByte, byte matchByte) { return m_Coders[GetState(pos, prevByte)].DecodeWithMatchByte(rangeDecoder, matchByte); } }; LZ.OutWindow m_OutWindow = new LZ.OutWindow(); RangeCoder.Decoder m_RangeDecoder = new RangeCoder.Decoder(); BitDecoder[] m_IsMatchDecoders = new BitDecoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; BitDecoder[] m_IsRepDecoders = new BitDecoder[Base.kNumStates]; BitDecoder[] m_IsRepG0Decoders = new BitDecoder[Base.kNumStates]; BitDecoder[] m_IsRepG1Decoders = new BitDecoder[Base.kNumStates]; BitDecoder[] m_IsRepG2Decoders = new BitDecoder[Base.kNumStates]; BitDecoder[] m_IsRep0LongDecoders = new BitDecoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; BitTreeDecoder[] m_PosSlotDecoder = new BitTreeDecoder[Base.kNumLenToPosStates]; BitDecoder[] m_PosDecoders = new BitDecoder[Base.kNumFullDistances - Base.kEndPosModelIndex]; BitTreeDecoder m_PosAlignDecoder = new BitTreeDecoder(Base.kNumAlignBits); LenDecoder m_LenDecoder = new LenDecoder(); LenDecoder m_RepLenDecoder = new LenDecoder(); LiteralDecoder m_LiteralDecoder = new LiteralDecoder(); uint m_DictionarySize; uint m_DictionarySizeCheck; uint m_PosStateMask; CancellationToken cancellationToken; public Decoder() { m_DictionarySize = 0xFFFFFFFF; for (int i = 0; i < Base.kNumLenToPosStates; i++) m_PosSlotDecoder[i] = new BitTreeDecoder(Base.kNumPosSlotBits); } public Decoder(CancellationToken cancellationToken) : this() { this.cancellationToken = cancellationToken; } void SetDictionarySize(uint dictionarySize) { if (m_DictionarySize != dictionarySize) { m_DictionarySize = dictionarySize; m_DictionarySizeCheck = Math.Max(m_DictionarySize, 1); uint blockSize = Math.Max(m_DictionarySizeCheck, (1 << 12)); m_OutWindow.Create(blockSize); } } void SetLiteralProperties(int lp, int lc) { if (lp > 8) throw new InvalidParamException(); if (lc > 8) throw new InvalidParamException(); m_LiteralDecoder.Create(lp, lc); } void SetPosBitsProperties(int pb) { if (pb > Base.kNumPosStatesBitsMax) throw new InvalidParamException(); uint numPosStates = (uint)1 << pb; m_LenDecoder.Create(numPosStates); m_RepLenDecoder.Create(numPosStates); m_PosStateMask = numPosStates - 1; } bool _solid = false; void Init(System.IO.Stream inStream, System.IO.Stream outStream) { m_RangeDecoder.Init(inStream); m_OutWindow.Init(outStream, _solid); uint i; for (i = 0; i < Base.kNumStates; i++) { for (uint j = 0; j <= m_PosStateMask; j++) { uint index = (i << Base.kNumPosStatesBitsMax) + j; m_IsMatchDecoders[index].Init(); m_IsRep0LongDecoders[index].Init(); } m_IsRepDecoders[i].Init(); m_IsRepG0Decoders[i].Init(); m_IsRepG1Decoders[i].Init(); m_IsRepG2Decoders[i].Init(); } m_LiteralDecoder.Init(); for (i = 0; i < Base.kNumLenToPosStates; i++) m_PosSlotDecoder[i].Init(); // m_PosSpecDecoder.Init(); for (i = 0; i < Base.kNumFullDistances - Base.kEndPosModelIndex; i++) m_PosDecoders[i].Init(); m_LenDecoder.Init(); m_RepLenDecoder.Init(); m_PosAlignDecoder.Init(); } public void Code(System.IO.Stream inStream, System.IO.Stream outStream, Int64 inSize, Int64 outSize, ICodeProgress progress) { Init(inStream, outStream); Base.State state = new Base.State(); state.Init(); uint rep0 = 0, rep1 = 0, rep2 = 0, rep3 = 0; UInt64 nowPos64 = 0; UInt64 outSize64 = (UInt64)outSize; if (nowPos64 < outSize64) { if (m_IsMatchDecoders[state.Index << Base.kNumPosStatesBitsMax].Decode(m_RangeDecoder) != 0) throw new DataErrorException(); state.UpdateChar(); byte b = m_LiteralDecoder.DecodeNormal(m_RangeDecoder, 0, 0); m_OutWindow.PutByte(b); nowPos64++; } while (nowPos64 < outSize64) { if (cancellationToken != null) cancellationToken.ThrowIfCancellationRequested(); // UInt64 next = Math.Min(nowPos64 + (1 << 18), outSize64); // while(nowPos64 < next) { uint posState = (uint)nowPos64 & m_PosStateMask; if (m_IsMatchDecoders[(state.Index << Base.kNumPosStatesBitsMax) + posState].Decode(m_RangeDecoder) == 0) { byte b; byte prevByte = m_OutWindow.GetByte(0); if (!state.IsCharState()) b = m_LiteralDecoder.DecodeWithMatchByte(m_RangeDecoder, (uint)nowPos64, prevByte, m_OutWindow.GetByte(rep0)); else b = m_LiteralDecoder.DecodeNormal(m_RangeDecoder, (uint)nowPos64, prevByte); m_OutWindow.PutByte(b); state.UpdateChar(); nowPos64++; } else { uint len; if (m_IsRepDecoders[state.Index].Decode(m_RangeDecoder) == 1) { if (m_IsRepG0Decoders[state.Index].Decode(m_RangeDecoder) == 0) { if (m_IsRep0LongDecoders[(state.Index << Base.kNumPosStatesBitsMax) + posState].Decode(m_RangeDecoder) == 0) { state.UpdateShortRep(); m_OutWindow.PutByte(m_OutWindow.GetByte(rep0)); nowPos64++; continue; } } else { UInt32 distance; if (m_IsRepG1Decoders[state.Index].Decode(m_RangeDecoder) == 0) { distance = rep1; } else { if (m_IsRepG2Decoders[state.Index].Decode(m_RangeDecoder) == 0) distance = rep2; else { distance = rep3; rep3 = rep2; } rep2 = rep1; } rep1 = rep0; rep0 = distance; } len = m_RepLenDecoder.Decode(m_RangeDecoder, posState) + Base.kMatchMinLen; state.UpdateRep(); } else { rep3 = rep2; rep2 = rep1; rep1 = rep0; len = Base.kMatchMinLen + m_LenDecoder.Decode(m_RangeDecoder, posState); state.UpdateMatch(); uint posSlot = m_PosSlotDecoder[Base.GetLenToPosState(len)].Decode(m_RangeDecoder); if (posSlot >= Base.kStartPosModelIndex) { int numDirectBits = (int)((posSlot >> 1) - 1); rep0 = ((2 | (posSlot & 1)) << numDirectBits); if (posSlot < Base.kEndPosModelIndex) rep0 += BitTreeDecoder.ReverseDecode(m_PosDecoders, rep0 - posSlot - 1, m_RangeDecoder, numDirectBits); else { rep0 += (m_RangeDecoder.DecodeDirectBits( numDirectBits - Base.kNumAlignBits) << Base.kNumAlignBits); rep0 += m_PosAlignDecoder.ReverseDecode(m_RangeDecoder); } } else rep0 = posSlot; } if (rep0 >= m_OutWindow.TrainSize + nowPos64 || rep0 >= m_DictionarySizeCheck) { if (rep0 == 0xFFFFFFFF) break; throw new DataErrorException(); } m_OutWindow.CopyBlock(rep0, len); nowPos64 += len; } } } m_OutWindow.Flush(); m_OutWindow.ReleaseStream(); m_RangeDecoder.ReleaseStream(); } public void SetDecoderProperties(byte[] properties) { if (properties.Length < 5) throw new InvalidParamException(); int lc = properties[0] % 9; int remainder = properties[0] / 9; int lp = remainder % 5; int pb = remainder / 5; if (pb > Base.kNumPosStatesBitsMax) throw new InvalidParamException(); UInt32 dictionarySize = 0; for (int i = 0; i < 4; i++) dictionarySize += ((UInt32)(properties[1 + i])) << (i * 8); SetDictionarySize(dictionarySize); SetLiteralProperties(lp, lc); SetPosBitsProperties(pb); } public bool Train(System.IO.Stream stream) { _solid = true; return m_OutWindow.Train(stream); } /* public override bool CanRead { get { return true; }} public override bool CanWrite { get { return true; }} public override bool CanSeek { get { return true; }} public override long Length { get { return 0; }} public override long Position { get { return 0; } set { } } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { return 0; } public override void Write(byte[] buffer, int offset, int count) { } public override long Seek(long offset, System.IO.SeekOrigin origin) { return 0; } public override void SetLength(long value) {} */ } } ================================================ FILE: ClientUpdater/Compression/LZMA/LzmaEncoder.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ #pragma warning disable CS1591, CS8073 // LzmaEncoder.cs using System; namespace SevenZip.Compression.LZMA { using System.Threading; using RangeCoder; public class Encoder : ICoder, ISetCoderProperties, IWriteCoderProperties { enum EMatchFinderType { BT2, BT4, }; const UInt32 kIfinityPrice = 0xFFFFFFF; static Byte[] g_FastPos = new Byte[1 << 11]; static Encoder() { const Byte kFastSlots = 22; int c = 2; g_FastPos[0] = 0; g_FastPos[1] = 1; for (Byte slotFast = 2; slotFast < kFastSlots; slotFast++) { UInt32 k = ((UInt32)1 << ((slotFast >> 1) - 1)); for (UInt32 j = 0; j < k; j++, c++) g_FastPos[c] = slotFast; } } static UInt32 GetPosSlot(UInt32 pos) { if (pos < (1 << 11)) return g_FastPos[pos]; if (pos < (1 << 21)) return (UInt32)(g_FastPos[pos >> 10] + 20); return (UInt32)(g_FastPos[pos >> 20] + 40); } static UInt32 GetPosSlot2(UInt32 pos) { if (pos < (1 << 17)) return (UInt32)(g_FastPos[pos >> 6] + 12); if (pos < (1 << 27)) return (UInt32)(g_FastPos[pos >> 16] + 32); return (UInt32)(g_FastPos[pos >> 26] + 52); } Base.State _state = new Base.State(); Byte _previousByte; UInt32[] _repDistances = new UInt32[Base.kNumRepDistances]; void BaseInit() { _state.Init(); _previousByte = 0; for (UInt32 i = 0; i < Base.kNumRepDistances; i++) _repDistances[i] = 0; } const int kDefaultDictionaryLogSize = 22; const UInt32 kNumFastBytesDefault = 0x20; class LiteralEncoder { public struct Encoder2 { BitEncoder[] m_Encoders; public void Create() { m_Encoders = new BitEncoder[0x300]; } public void Init() { for (int i = 0; i < 0x300; i++) m_Encoders[i].Init(); } public void Encode(RangeCoder.Encoder rangeEncoder, byte symbol) { uint context = 1; for (int i = 7; i >= 0; i--) { uint bit = (uint)((symbol >> i) & 1); m_Encoders[context].Encode(rangeEncoder, bit); context = (context << 1) | bit; } } public void EncodeMatched(RangeCoder.Encoder rangeEncoder, byte matchByte, byte symbol) { uint context = 1; bool same = true; for (int i = 7; i >= 0; i--) { uint bit = (uint)((symbol >> i) & 1); uint state = context; if (same) { uint matchBit = (uint)((matchByte >> i) & 1); state += ((1 + matchBit) << 8); same = (matchBit == bit); } m_Encoders[state].Encode(rangeEncoder, bit); context = (context << 1) | bit; } } public uint GetPrice(bool matchMode, byte matchByte, byte symbol) { uint price = 0; uint context = 1; int i = 7; if (matchMode) { for (; i >= 0; i--) { uint matchBit = (uint)(matchByte >> i) & 1; uint bit = (uint)(symbol >> i) & 1; price += m_Encoders[((1 + matchBit) << 8) + context].GetPrice(bit); context = (context << 1) | bit; if (matchBit != bit) { i--; break; } } } for (; i >= 0; i--) { uint bit = (uint)(symbol >> i) & 1; price += m_Encoders[context].GetPrice(bit); context = (context << 1) | bit; } return price; } } Encoder2[] m_Coders; int m_NumPrevBits; int m_NumPosBits; uint m_PosMask; public void Create(int numPosBits, int numPrevBits) { if (m_Coders != null && m_NumPrevBits == numPrevBits && m_NumPosBits == numPosBits) return; m_NumPosBits = numPosBits; m_PosMask = ((uint)1 << numPosBits) - 1; m_NumPrevBits = numPrevBits; uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); m_Coders = new Encoder2[numStates]; for (uint i = 0; i < numStates; i++) m_Coders[i].Create(); } public void Init() { uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); for (uint i = 0; i < numStates; i++) m_Coders[i].Init(); } public Encoder2 GetSubCoder(UInt32 pos, Byte prevByte) { return m_Coders[((pos & m_PosMask) << m_NumPrevBits) + (uint)(prevByte >> (8 - m_NumPrevBits))]; } } class LenEncoder { RangeCoder.BitEncoder _choice = new RangeCoder.BitEncoder(); RangeCoder.BitEncoder _choice2 = new RangeCoder.BitEncoder(); RangeCoder.BitTreeEncoder[] _lowCoder = new RangeCoder.BitTreeEncoder[Base.kNumPosStatesEncodingMax]; RangeCoder.BitTreeEncoder[] _midCoder = new RangeCoder.BitTreeEncoder[Base.kNumPosStatesEncodingMax]; RangeCoder.BitTreeEncoder _highCoder = new RangeCoder.BitTreeEncoder(Base.kNumHighLenBits); public LenEncoder() { for (UInt32 posState = 0; posState < Base.kNumPosStatesEncodingMax; posState++) { _lowCoder[posState] = new RangeCoder.BitTreeEncoder(Base.kNumLowLenBits); _midCoder[posState] = new RangeCoder.BitTreeEncoder(Base.kNumMidLenBits); } } public void Init(UInt32 numPosStates) { _choice.Init(); _choice2.Init(); for (UInt32 posState = 0; posState < numPosStates; posState++) { _lowCoder[posState].Init(); _midCoder[posState].Init(); } _highCoder.Init(); } public void Encode(RangeCoder.Encoder rangeEncoder, UInt32 symbol, UInt32 posState) { if (symbol < Base.kNumLowLenSymbols) { _choice.Encode(rangeEncoder, 0); _lowCoder[posState].Encode(rangeEncoder, symbol); } else { symbol -= Base.kNumLowLenSymbols; _choice.Encode(rangeEncoder, 1); if (symbol < Base.kNumMidLenSymbols) { _choice2.Encode(rangeEncoder, 0); _midCoder[posState].Encode(rangeEncoder, symbol); } else { _choice2.Encode(rangeEncoder, 1); _highCoder.Encode(rangeEncoder, symbol - Base.kNumMidLenSymbols); } } } public void SetPrices(UInt32 posState, UInt32 numSymbols, UInt32[] prices, UInt32 st) { UInt32 a0 = _choice.GetPrice0(); UInt32 a1 = _choice.GetPrice1(); UInt32 b0 = a1 + _choice2.GetPrice0(); UInt32 b1 = a1 + _choice2.GetPrice1(); UInt32 i = 0; for (i = 0; i < Base.kNumLowLenSymbols; i++) { if (i >= numSymbols) return; prices[st + i] = a0 + _lowCoder[posState].GetPrice(i); } for (; i < Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; i++) { if (i >= numSymbols) return; prices[st + i] = b0 + _midCoder[posState].GetPrice(i - Base.kNumLowLenSymbols); } for (; i < numSymbols; i++) prices[st + i] = b1 + _highCoder.GetPrice(i - Base.kNumLowLenSymbols - Base.kNumMidLenSymbols); } }; const UInt32 kNumLenSpecSymbols = Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; class LenPriceTableEncoder : LenEncoder { UInt32[] _prices = new UInt32[Base.kNumLenSymbols << Base.kNumPosStatesBitsEncodingMax]; UInt32 _tableSize; UInt32[] _counters = new UInt32[Base.kNumPosStatesEncodingMax]; public void SetTableSize(UInt32 tableSize) { _tableSize = tableSize; } public UInt32 GetPrice(UInt32 symbol, UInt32 posState) { return _prices[posState * Base.kNumLenSymbols + symbol]; } void UpdateTable(UInt32 posState) { SetPrices(posState, _tableSize, _prices, posState * Base.kNumLenSymbols); _counters[posState] = _tableSize; } public void UpdateTables(UInt32 numPosStates) { for (UInt32 posState = 0; posState < numPosStates; posState++) UpdateTable(posState); } public new void Encode(RangeCoder.Encoder rangeEncoder, UInt32 symbol, UInt32 posState) { base.Encode(rangeEncoder, symbol, posState); if (--_counters[posState] == 0) UpdateTable(posState); } } const UInt32 kNumOpts = 1 << 12; class Optimal { public Base.State State; public bool Prev1IsChar; public bool Prev2; public UInt32 PosPrev2; public UInt32 BackPrev2; public UInt32 Price; public UInt32 PosPrev; public UInt32 BackPrev; public UInt32 Backs0; public UInt32 Backs1; public UInt32 Backs2; public UInt32 Backs3; public void MakeAsChar() { BackPrev = 0xFFFFFFFF; Prev1IsChar = false; } public void MakeAsShortRep() { BackPrev = 0; ; Prev1IsChar = false; } public bool IsShortRep() { return (BackPrev == 0); } }; Optimal[] _optimum = new Optimal[kNumOpts]; LZ.IMatchFinder _matchFinder = null; RangeCoder.Encoder _rangeEncoder = new RangeCoder.Encoder(); RangeCoder.BitEncoder[] _isMatch = new RangeCoder.BitEncoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; RangeCoder.BitEncoder[] _isRep = new RangeCoder.BitEncoder[Base.kNumStates]; RangeCoder.BitEncoder[] _isRepG0 = new RangeCoder.BitEncoder[Base.kNumStates]; RangeCoder.BitEncoder[] _isRepG1 = new RangeCoder.BitEncoder[Base.kNumStates]; RangeCoder.BitEncoder[] _isRepG2 = new RangeCoder.BitEncoder[Base.kNumStates]; RangeCoder.BitEncoder[] _isRep0Long = new RangeCoder.BitEncoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; RangeCoder.BitTreeEncoder[] _posSlotEncoder = new RangeCoder.BitTreeEncoder[Base.kNumLenToPosStates]; RangeCoder.BitEncoder[] _posEncoders = new RangeCoder.BitEncoder[Base.kNumFullDistances - Base.kEndPosModelIndex]; RangeCoder.BitTreeEncoder _posAlignEncoder = new RangeCoder.BitTreeEncoder(Base.kNumAlignBits); LenPriceTableEncoder _lenEncoder = new LenPriceTableEncoder(); LenPriceTableEncoder _repMatchLenEncoder = new LenPriceTableEncoder(); LiteralEncoder _literalEncoder = new LiteralEncoder(); UInt32[] _matchDistances = new UInt32[Base.kMatchMaxLen * 2 + 2]; UInt32 _numFastBytes = kNumFastBytesDefault; UInt32 _longestMatchLength; UInt32 _numDistancePairs; UInt32 _additionalOffset; UInt32 _optimumEndIndex; UInt32 _optimumCurrentIndex; bool _longestMatchWasFound; UInt32[] _posSlotPrices = new UInt32[1 << (Base.kNumPosSlotBits + Base.kNumLenToPosStatesBits)]; UInt32[] _distancesPrices = new UInt32[Base.kNumFullDistances << Base.kNumLenToPosStatesBits]; UInt32[] _alignPrices = new UInt32[Base.kAlignTableSize]; UInt32 _alignPriceCount; UInt32 _distTableSize = (kDefaultDictionaryLogSize * 2); int _posStateBits = 2; UInt32 _posStateMask = (4 - 1); int _numLiteralPosStateBits = 0; int _numLiteralContextBits = 3; UInt32 _dictionarySize = (1 << kDefaultDictionaryLogSize); UInt32 _dictionarySizePrev = 0xFFFFFFFF; UInt32 _numFastBytesPrev = 0xFFFFFFFF; Int64 nowPos64; bool _finished; System.IO.Stream _inStream; EMatchFinderType _matchFinderType = EMatchFinderType.BT4; bool _writeEndMark = false; bool _needReleaseMFStream; CancellationToken cancellationToken; void Create() { if (_matchFinder == null) { LZ.BinTree bt = new LZ.BinTree(); int numHashBytes = 4; if (_matchFinderType == EMatchFinderType.BT2) numHashBytes = 2; bt.SetType(numHashBytes); _matchFinder = bt; } _literalEncoder.Create(_numLiteralPosStateBits, _numLiteralContextBits); if (_dictionarySize == _dictionarySizePrev && _numFastBytesPrev == _numFastBytes) return; _matchFinder.Create(_dictionarySize, kNumOpts, _numFastBytes, Base.kMatchMaxLen + 1); _dictionarySizePrev = _dictionarySize; _numFastBytesPrev = _numFastBytes; } public Encoder() { for (int i = 0; i < kNumOpts; i++) _optimum[i] = new Optimal(); for (int i = 0; i < Base.kNumLenToPosStates; i++) _posSlotEncoder[i] = new RangeCoder.BitTreeEncoder(Base.kNumPosSlotBits); } public Encoder(CancellationToken cancellationToken) : this() { this.cancellationToken = cancellationToken; } void SetWriteEndMarkerMode(bool writeEndMarker) { _writeEndMark = writeEndMarker; } void Init() { BaseInit(); _rangeEncoder.Init(); uint i; for (i = 0; i < Base.kNumStates; i++) { for (uint j = 0; j <= _posStateMask; j++) { uint complexState = (i << Base.kNumPosStatesBitsMax) + j; _isMatch[complexState].Init(); _isRep0Long[complexState].Init(); } _isRep[i].Init(); _isRepG0[i].Init(); _isRepG1[i].Init(); _isRepG2[i].Init(); } _literalEncoder.Init(); for (i = 0; i < Base.kNumLenToPosStates; i++) _posSlotEncoder[i].Init(); for (i = 0; i < Base.kNumFullDistances - Base.kEndPosModelIndex; i++) _posEncoders[i].Init(); _lenEncoder.Init((UInt32)1 << _posStateBits); _repMatchLenEncoder.Init((UInt32)1 << _posStateBits); _posAlignEncoder.Init(); _longestMatchWasFound = false; _optimumEndIndex = 0; _optimumCurrentIndex = 0; _additionalOffset = 0; } void ReadMatchDistances(out UInt32 lenRes, out UInt32 numDistancePairs) { lenRes = 0; numDistancePairs = _matchFinder.GetMatches(_matchDistances); if (numDistancePairs > 0) { lenRes = _matchDistances[numDistancePairs - 2]; if (lenRes == _numFastBytes) lenRes += _matchFinder.GetMatchLen((int)lenRes - 1, _matchDistances[numDistancePairs - 1], Base.kMatchMaxLen - lenRes); } _additionalOffset++; } void MovePos(UInt32 num) { if (num > 0) { _matchFinder.Skip(num); _additionalOffset += num; } } UInt32 GetRepLen1Price(Base.State state, UInt32 posState) { return _isRepG0[state.Index].GetPrice0() + _isRep0Long[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0(); } UInt32 GetPureRepPrice(UInt32 repIndex, Base.State state, UInt32 posState) { UInt32 price; if (repIndex == 0) { price = _isRepG0[state.Index].GetPrice0(); price += _isRep0Long[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1(); } else { price = _isRepG0[state.Index].GetPrice1(); if (repIndex == 1) price += _isRepG1[state.Index].GetPrice0(); else { price += _isRepG1[state.Index].GetPrice1(); price += _isRepG2[state.Index].GetPrice(repIndex - 2); } } return price; } UInt32 GetRepPrice(UInt32 repIndex, UInt32 len, Base.State state, UInt32 posState) { UInt32 price = _repMatchLenEncoder.GetPrice(len - Base.kMatchMinLen, posState); return price + GetPureRepPrice(repIndex, state, posState); } UInt32 GetPosLenPrice(UInt32 pos, UInt32 len, UInt32 posState) { UInt32 price; UInt32 lenToPosState = Base.GetLenToPosState(len); if (pos < Base.kNumFullDistances) price = _distancesPrices[(lenToPosState * Base.kNumFullDistances) + pos]; else price = _posSlotPrices[(lenToPosState << Base.kNumPosSlotBits) + GetPosSlot2(pos)] + _alignPrices[pos & Base.kAlignMask]; return price + _lenEncoder.GetPrice(len - Base.kMatchMinLen, posState); } UInt32 Backward(out UInt32 backRes, UInt32 cur) { _optimumEndIndex = cur; UInt32 posMem = _optimum[cur].PosPrev; UInt32 backMem = _optimum[cur].BackPrev; do { if (_optimum[cur].Prev1IsChar) { _optimum[posMem].MakeAsChar(); _optimum[posMem].PosPrev = posMem - 1; if (_optimum[cur].Prev2) { _optimum[posMem - 1].Prev1IsChar = false; _optimum[posMem - 1].PosPrev = _optimum[cur].PosPrev2; _optimum[posMem - 1].BackPrev = _optimum[cur].BackPrev2; } } UInt32 posPrev = posMem; UInt32 backCur = backMem; backMem = _optimum[posPrev].BackPrev; posMem = _optimum[posPrev].PosPrev; _optimum[posPrev].BackPrev = backCur; _optimum[posPrev].PosPrev = cur; cur = posPrev; } while (cur > 0); backRes = _optimum[0].BackPrev; _optimumCurrentIndex = _optimum[0].PosPrev; return _optimumCurrentIndex; } UInt32[] reps = new UInt32[Base.kNumRepDistances]; UInt32[] repLens = new UInt32[Base.kNumRepDistances]; UInt32 GetOptimum(UInt32 position, out UInt32 backRes) { if (_optimumEndIndex != _optimumCurrentIndex) { UInt32 lenRes = _optimum[_optimumCurrentIndex].PosPrev - _optimumCurrentIndex; backRes = _optimum[_optimumCurrentIndex].BackPrev; _optimumCurrentIndex = _optimum[_optimumCurrentIndex].PosPrev; return lenRes; } _optimumCurrentIndex = _optimumEndIndex = 0; UInt32 lenMain, numDistancePairs; if (!_longestMatchWasFound) { ReadMatchDistances(out lenMain, out numDistancePairs); } else { lenMain = _longestMatchLength; numDistancePairs = _numDistancePairs; _longestMatchWasFound = false; } UInt32 numAvailableBytes = _matchFinder.GetNumAvailableBytes() + 1; if (numAvailableBytes < 2) { backRes = 0xFFFFFFFF; return 1; } if (numAvailableBytes > Base.kMatchMaxLen) numAvailableBytes = Base.kMatchMaxLen; UInt32 repMaxIndex = 0; UInt32 i; for (i = 0; i < Base.kNumRepDistances; i++) { reps[i] = _repDistances[i]; repLens[i] = _matchFinder.GetMatchLen(0 - 1, reps[i], Base.kMatchMaxLen); if (repLens[i] > repLens[repMaxIndex]) repMaxIndex = i; } if (repLens[repMaxIndex] >= _numFastBytes) { backRes = repMaxIndex; UInt32 lenRes = repLens[repMaxIndex]; MovePos(lenRes - 1); return lenRes; } if (lenMain >= _numFastBytes) { backRes = _matchDistances[numDistancePairs - 1] + Base.kNumRepDistances; MovePos(lenMain - 1); return lenMain; } Byte currentByte = _matchFinder.GetIndexByte(0 - 1); Byte matchByte = _matchFinder.GetIndexByte((Int32)(0 - _repDistances[0] - 1 - 1)); if (lenMain < 2 && currentByte != matchByte && repLens[repMaxIndex] < 2) { backRes = (UInt32)0xFFFFFFFF; return 1; } _optimum[0].State = _state; UInt32 posState = (position & _posStateMask); _optimum[1].Price = _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0() + _literalEncoder.GetSubCoder(position, _previousByte).GetPrice(!_state.IsCharState(), matchByte, currentByte); _optimum[1].MakeAsChar(); UInt32 matchPrice = _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1(); UInt32 repMatchPrice = matchPrice + _isRep[_state.Index].GetPrice1(); if (matchByte == currentByte) { UInt32 shortRepPrice = repMatchPrice + GetRepLen1Price(_state, posState); if (shortRepPrice < _optimum[1].Price) { _optimum[1].Price = shortRepPrice; _optimum[1].MakeAsShortRep(); } } UInt32 lenEnd = ((lenMain >= repLens[repMaxIndex]) ? lenMain : repLens[repMaxIndex]); if(lenEnd < 2) { backRes = _optimum[1].BackPrev; return 1; } _optimum[1].PosPrev = 0; _optimum[0].Backs0 = reps[0]; _optimum[0].Backs1 = reps[1]; _optimum[0].Backs2 = reps[2]; _optimum[0].Backs3 = reps[3]; UInt32 len = lenEnd; do _optimum[len--].Price = kIfinityPrice; while (len >= 2); for (i = 0; i < Base.kNumRepDistances; i++) { UInt32 repLen = repLens[i]; if (repLen < 2) continue; UInt32 price = repMatchPrice + GetPureRepPrice(i, _state, posState); do { UInt32 curAndLenPrice = price + _repMatchLenEncoder.GetPrice(repLen - 2, posState); Optimal optimum = _optimum[repLen]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = 0; optimum.BackPrev = i; optimum.Prev1IsChar = false; } } while (--repLen >= 2); } UInt32 normalMatchPrice = matchPrice + _isRep[_state.Index].GetPrice0(); len = ((repLens[0] >= 2) ? repLens[0] + 1 : 2); if (len <= lenMain) { UInt32 offs = 0; while (len > _matchDistances[offs]) offs += 2; for (; ; len++) { UInt32 distance = _matchDistances[offs + 1]; UInt32 curAndLenPrice = normalMatchPrice + GetPosLenPrice(distance, len, posState); Optimal optimum = _optimum[len]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = 0; optimum.BackPrev = distance + Base.kNumRepDistances; optimum.Prev1IsChar = false; } if (len == _matchDistances[offs]) { offs += 2; if (offs == numDistancePairs) break; } } } UInt32 cur = 0; while (true) { cur++; if (cur == lenEnd) return Backward(out backRes, cur); UInt32 newLen; ReadMatchDistances(out newLen, out numDistancePairs); if (newLen >= _numFastBytes) { _numDistancePairs = numDistancePairs; _longestMatchLength = newLen; _longestMatchWasFound = true; return Backward(out backRes, cur); } position++; UInt32 posPrev = _optimum[cur].PosPrev; Base.State state; if (_optimum[cur].Prev1IsChar) { posPrev--; if (_optimum[cur].Prev2) { state = _optimum[_optimum[cur].PosPrev2].State; if (_optimum[cur].BackPrev2 < Base.kNumRepDistances) state.UpdateRep(); else state.UpdateMatch(); } else state = _optimum[posPrev].State; state.UpdateChar(); } else state = _optimum[posPrev].State; if (posPrev == cur - 1) { if (_optimum[cur].IsShortRep()) state.UpdateShortRep(); else state.UpdateChar(); } else { UInt32 pos; if (_optimum[cur].Prev1IsChar && _optimum[cur].Prev2) { posPrev = _optimum[cur].PosPrev2; pos = _optimum[cur].BackPrev2; state.UpdateRep(); } else { pos = _optimum[cur].BackPrev; if (pos < Base.kNumRepDistances) state.UpdateRep(); else state.UpdateMatch(); } Optimal opt = _optimum[posPrev]; if (pos < Base.kNumRepDistances) { if (pos == 0) { reps[0] = opt.Backs0; reps[1] = opt.Backs1; reps[2] = opt.Backs2; reps[3] = opt.Backs3; } else if (pos == 1) { reps[0] = opt.Backs1; reps[1] = opt.Backs0; reps[2] = opt.Backs2; reps[3] = opt.Backs3; } else if (pos == 2) { reps[0] = opt.Backs2; reps[1] = opt.Backs0; reps[2] = opt.Backs1; reps[3] = opt.Backs3; } else { reps[0] = opt.Backs3; reps[1] = opt.Backs0; reps[2] = opt.Backs1; reps[3] = opt.Backs2; } } else { reps[0] = (pos - Base.kNumRepDistances); reps[1] = opt.Backs0; reps[2] = opt.Backs1; reps[3] = opt.Backs2; } } _optimum[cur].State = state; _optimum[cur].Backs0 = reps[0]; _optimum[cur].Backs1 = reps[1]; _optimum[cur].Backs2 = reps[2]; _optimum[cur].Backs3 = reps[3]; UInt32 curPrice = _optimum[cur].Price; currentByte = _matchFinder.GetIndexByte(0 - 1); matchByte = _matchFinder.GetIndexByte((Int32)(0 - reps[0] - 1 - 1)); posState = (position & _posStateMask); UInt32 curAnd1Price = curPrice + _isMatch[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0() + _literalEncoder.GetSubCoder(position, _matchFinder.GetIndexByte(0 - 2)). GetPrice(!state.IsCharState(), matchByte, currentByte); Optimal nextOptimum = _optimum[cur + 1]; bool nextIsChar = false; if (curAnd1Price < nextOptimum.Price) { nextOptimum.Price = curAnd1Price; nextOptimum.PosPrev = cur; nextOptimum.MakeAsChar(); nextIsChar = true; } matchPrice = curPrice + _isMatch[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1(); repMatchPrice = matchPrice + _isRep[state.Index].GetPrice1(); if (matchByte == currentByte && !(nextOptimum.PosPrev < cur && nextOptimum.BackPrev == 0)) { UInt32 shortRepPrice = repMatchPrice + GetRepLen1Price(state, posState); if (shortRepPrice <= nextOptimum.Price) { nextOptimum.Price = shortRepPrice; nextOptimum.PosPrev = cur; nextOptimum.MakeAsShortRep(); nextIsChar = true; } } UInt32 numAvailableBytesFull = _matchFinder.GetNumAvailableBytes() + 1; numAvailableBytesFull = Math.Min(kNumOpts - 1 - cur, numAvailableBytesFull); numAvailableBytes = numAvailableBytesFull; if (numAvailableBytes < 2) continue; if (numAvailableBytes > _numFastBytes) numAvailableBytes = _numFastBytes; if (!nextIsChar && matchByte != currentByte) { // try Literal + rep0 UInt32 t = Math.Min(numAvailableBytesFull - 1, _numFastBytes); UInt32 lenTest2 = _matchFinder.GetMatchLen(0, reps[0], t); if (lenTest2 >= 2) { Base.State state2 = state; state2.UpdateChar(); UInt32 posStateNext = (position + 1) & _posStateMask; UInt32 nextRepMatchPrice = curAnd1Price + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1() + _isRep[state2.Index].GetPrice1(); { UInt32 offset = cur + 1 + lenTest2; while (lenEnd < offset) _optimum[++lenEnd].Price = kIfinityPrice; UInt32 curAndLenPrice = nextRepMatchPrice + GetRepPrice( 0, lenTest2, state2, posStateNext); Optimal optimum = _optimum[offset]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur + 1; optimum.BackPrev = 0; optimum.Prev1IsChar = true; optimum.Prev2 = false; } } } } UInt32 startLen = 2; // speed optimization for (UInt32 repIndex = 0; repIndex < Base.kNumRepDistances; repIndex++) { UInt32 lenTest = _matchFinder.GetMatchLen(0 - 1, reps[repIndex], numAvailableBytes); if (lenTest < 2) continue; UInt32 lenTestTemp = lenTest; do { while (lenEnd < cur + lenTest) _optimum[++lenEnd].Price = kIfinityPrice; UInt32 curAndLenPrice = repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState); Optimal optimum = _optimum[cur + lenTest]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur; optimum.BackPrev = repIndex; optimum.Prev1IsChar = false; } } while(--lenTest >= 2); lenTest = lenTestTemp; if (repIndex == 0) startLen = lenTest + 1; // if (_maxMode) if (lenTest < numAvailableBytesFull) { UInt32 t = Math.Min(numAvailableBytesFull - 1 - lenTest, _numFastBytes); UInt32 lenTest2 = _matchFinder.GetMatchLen((Int32)lenTest, reps[repIndex], t); if (lenTest2 >= 2) { Base.State state2 = state; state2.UpdateRep(); UInt32 posStateNext = (position + lenTest) & _posStateMask; UInt32 curAndLenCharPrice = repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState) + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice0() + _literalEncoder.GetSubCoder(position + lenTest, _matchFinder.GetIndexByte((Int32)lenTest - 1 - 1)).GetPrice(true, _matchFinder.GetIndexByte((Int32)((Int32)lenTest - 1 - (Int32)(reps[repIndex] + 1))), _matchFinder.GetIndexByte((Int32)lenTest - 1)); state2.UpdateChar(); posStateNext = (position + lenTest + 1) & _posStateMask; UInt32 nextMatchPrice = curAndLenCharPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1(); UInt32 nextRepMatchPrice = nextMatchPrice + _isRep[state2.Index].GetPrice1(); // for(; lenTest2 >= 2; lenTest2--) { UInt32 offset = lenTest + 1 + lenTest2; while(lenEnd < cur + offset) _optimum[++lenEnd].Price = kIfinityPrice; UInt32 curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext); Optimal optimum = _optimum[cur + offset]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur + lenTest + 1; optimum.BackPrev = 0; optimum.Prev1IsChar = true; optimum.Prev2 = true; optimum.PosPrev2 = cur; optimum.BackPrev2 = repIndex; } } } } } if (newLen > numAvailableBytes) { newLen = numAvailableBytes; for (numDistancePairs = 0; newLen > _matchDistances[numDistancePairs]; numDistancePairs += 2) ; _matchDistances[numDistancePairs] = newLen; numDistancePairs += 2; } if (newLen >= startLen) { normalMatchPrice = matchPrice + _isRep[state.Index].GetPrice0(); while (lenEnd < cur + newLen) _optimum[++lenEnd].Price = kIfinityPrice; UInt32 offs = 0; while (startLen > _matchDistances[offs]) offs += 2; for (UInt32 lenTest = startLen; ; lenTest++) { UInt32 curBack = _matchDistances[offs + 1]; UInt32 curAndLenPrice = normalMatchPrice + GetPosLenPrice(curBack, lenTest, posState); Optimal optimum = _optimum[cur + lenTest]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur; optimum.BackPrev = curBack + Base.kNumRepDistances; optimum.Prev1IsChar = false; } if (lenTest == _matchDistances[offs]) { if (lenTest < numAvailableBytesFull) { UInt32 t = Math.Min(numAvailableBytesFull - 1 - lenTest, _numFastBytes); UInt32 lenTest2 = _matchFinder.GetMatchLen((Int32)lenTest, curBack, t); if (lenTest2 >= 2) { Base.State state2 = state; state2.UpdateMatch(); UInt32 posStateNext = (position + lenTest) & _posStateMask; UInt32 curAndLenCharPrice = curAndLenPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice0() + _literalEncoder.GetSubCoder(position + lenTest, _matchFinder.GetIndexByte((Int32)lenTest - 1 - 1)). GetPrice(true, _matchFinder.GetIndexByte((Int32)lenTest - (Int32)(curBack + 1) - 1), _matchFinder.GetIndexByte((Int32)lenTest - 1)); state2.UpdateChar(); posStateNext = (position + lenTest + 1) & _posStateMask; UInt32 nextMatchPrice = curAndLenCharPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1(); UInt32 nextRepMatchPrice = nextMatchPrice + _isRep[state2.Index].GetPrice1(); UInt32 offset = lenTest + 1 + lenTest2; while (lenEnd < cur + offset) _optimum[++lenEnd].Price = kIfinityPrice; curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext); optimum = _optimum[cur + offset]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur + lenTest + 1; optimum.BackPrev = 0; optimum.Prev1IsChar = true; optimum.Prev2 = true; optimum.PosPrev2 = cur; optimum.BackPrev2 = curBack + Base.kNumRepDistances; } } } offs += 2; if (offs == numDistancePairs) break; } } } } } bool ChangePair(UInt32 smallDist, UInt32 bigDist) { const int kDif = 7; return (smallDist < ((UInt32)(1) << (32 - kDif)) && bigDist >= (smallDist << kDif)); } void WriteEndMarker(UInt32 posState) { if (!_writeEndMark) return; _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].Encode(_rangeEncoder, 1); _isRep[_state.Index].Encode(_rangeEncoder, 0); _state.UpdateMatch(); UInt32 len = Base.kMatchMinLen; _lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); UInt32 posSlot = (1 << Base.kNumPosSlotBits) - 1; UInt32 lenToPosState = Base.GetLenToPosState(len); _posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot); int footerBits = 30; UInt32 posReduced = (((UInt32)1) << footerBits) - 1; _rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits); _posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask); } void Flush(UInt32 nowPos) { ReleaseMFStream(); WriteEndMarker(nowPos & _posStateMask); _rangeEncoder.FlushData(); _rangeEncoder.FlushStream(); } public void CodeOneBlock(out Int64 inSize, out Int64 outSize, out bool finished) { inSize = 0; outSize = 0; finished = true; if (_inStream != null) { _matchFinder.SetStream(_inStream); _matchFinder.Init(); _needReleaseMFStream = true; _inStream = null; if (_trainSize > 0) _matchFinder.Skip(_trainSize); } if (_finished) return; _finished = true; Int64 progressPosValuePrev = nowPos64; if (nowPos64 == 0) { if (_matchFinder.GetNumAvailableBytes() == 0) { Flush((UInt32)nowPos64); return; } UInt32 len, numDistancePairs; // it's not used ReadMatchDistances(out len, out numDistancePairs); UInt32 posState = (UInt32)(nowPos64) & _posStateMask; _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].Encode(_rangeEncoder, 0); _state.UpdateChar(); Byte curByte = _matchFinder.GetIndexByte((Int32)(0 - _additionalOffset)); _literalEncoder.GetSubCoder((UInt32)(nowPos64), _previousByte).Encode(_rangeEncoder, curByte); _previousByte = curByte; _additionalOffset--; nowPos64++; } if (_matchFinder.GetNumAvailableBytes() == 0) { Flush((UInt32)nowPos64); return; } while (true) { UInt32 pos; UInt32 len = GetOptimum((UInt32)nowPos64, out pos); UInt32 posState = ((UInt32)nowPos64) & _posStateMask; UInt32 complexState = (_state.Index << Base.kNumPosStatesBitsMax) + posState; if (len == 1 && pos == 0xFFFFFFFF) { _isMatch[complexState].Encode(_rangeEncoder, 0); Byte curByte = _matchFinder.GetIndexByte((Int32)(0 - _additionalOffset)); LiteralEncoder.Encoder2 subCoder = _literalEncoder.GetSubCoder((UInt32)nowPos64, _previousByte); if (!_state.IsCharState()) { Byte matchByte = _matchFinder.GetIndexByte((Int32)(0 - _repDistances[0] - 1 - _additionalOffset)); subCoder.EncodeMatched(_rangeEncoder, matchByte, curByte); } else subCoder.Encode(_rangeEncoder, curByte); _previousByte = curByte; _state.UpdateChar(); } else { _isMatch[complexState].Encode(_rangeEncoder, 1); if (pos < Base.kNumRepDistances) { _isRep[_state.Index].Encode(_rangeEncoder, 1); if (pos == 0) { _isRepG0[_state.Index].Encode(_rangeEncoder, 0); if (len == 1) _isRep0Long[complexState].Encode(_rangeEncoder, 0); else _isRep0Long[complexState].Encode(_rangeEncoder, 1); } else { _isRepG0[_state.Index].Encode(_rangeEncoder, 1); if (pos == 1) _isRepG1[_state.Index].Encode(_rangeEncoder, 0); else { _isRepG1[_state.Index].Encode(_rangeEncoder, 1); _isRepG2[_state.Index].Encode(_rangeEncoder, pos - 2); } } if (len == 1) _state.UpdateShortRep(); else { _repMatchLenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); _state.UpdateRep(); } UInt32 distance = _repDistances[pos]; if (pos != 0) { for (UInt32 i = pos; i >= 1; i--) _repDistances[i] = _repDistances[i - 1]; _repDistances[0] = distance; } } else { _isRep[_state.Index].Encode(_rangeEncoder, 0); _state.UpdateMatch(); _lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); pos -= Base.kNumRepDistances; UInt32 posSlot = GetPosSlot(pos); UInt32 lenToPosState = Base.GetLenToPosState(len); _posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot); if (posSlot >= Base.kStartPosModelIndex) { int footerBits = (int)((posSlot >> 1) - 1); UInt32 baseVal = ((2 | (posSlot & 1)) << footerBits); UInt32 posReduced = pos - baseVal; if (posSlot < Base.kEndPosModelIndex) RangeCoder.BitTreeEncoder.ReverseEncode(_posEncoders, baseVal - posSlot - 1, _rangeEncoder, footerBits, posReduced); else { _rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits); _posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask); _alignPriceCount++; } } UInt32 distance = pos; for (UInt32 i = Base.kNumRepDistances - 1; i >= 1; i--) _repDistances[i] = _repDistances[i - 1]; _repDistances[0] = distance; _matchPriceCount++; } _previousByte = _matchFinder.GetIndexByte((Int32)(len - 1 - _additionalOffset)); } _additionalOffset -= len; nowPos64 += len; if (_additionalOffset == 0) { // if (!_fastMode) if (_matchPriceCount >= (1 << 7)) FillDistancesPrices(); if (_alignPriceCount >= Base.kAlignTableSize) FillAlignPrices(); inSize = nowPos64; outSize = _rangeEncoder.GetProcessedSizeAdd(); if (_matchFinder.GetNumAvailableBytes() == 0) { Flush((UInt32)nowPos64); return; } if (nowPos64 - progressPosValuePrev >= (1 << 12)) { _finished = false; finished = false; return; } } } } void ReleaseMFStream() { if (_matchFinder != null && _needReleaseMFStream) { _matchFinder.ReleaseStream(); _needReleaseMFStream = false; } } void SetOutStream(System.IO.Stream outStream) { _rangeEncoder.SetStream(outStream); } void ReleaseOutStream() { _rangeEncoder.ReleaseStream(); } void ReleaseStreams() { ReleaseMFStream(); ReleaseOutStream(); } void SetStreams(System.IO.Stream inStream, System.IO.Stream outStream, Int64 inSize, Int64 outSize) { _inStream = inStream; _finished = false; Create(); SetOutStream(outStream); Init(); // if (!_fastMode) { FillDistancesPrices(); FillAlignPrices(); } _lenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen); _lenEncoder.UpdateTables((UInt32)1 << _posStateBits); _repMatchLenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen); _repMatchLenEncoder.UpdateTables((UInt32)1 << _posStateBits); nowPos64 = 0; } public void Code(System.IO.Stream inStream, System.IO.Stream outStream, Int64 inSize, Int64 outSize, ICodeProgress progress) { _needReleaseMFStream = false; try { SetStreams(inStream, outStream, inSize, outSize); while (true) { if (cancellationToken != null) cancellationToken.ThrowIfCancellationRequested(); Int64 processedInSize; Int64 processedOutSize; bool finished; CodeOneBlock(out processedInSize, out processedOutSize, out finished); if (finished) return; if (progress != null) { progress.SetProgress(processedInSize, processedOutSize); } } } finally { ReleaseStreams(); } } const int kPropSize = 5; Byte[] properties = new Byte[kPropSize]; public void WriteCoderProperties(System.IO.Stream outStream) { properties[0] = (Byte)((_posStateBits * 5 + _numLiteralPosStateBits) * 9 + _numLiteralContextBits); for (int i = 0; i < 4; i++) properties[1 + i] = (Byte)((_dictionarySize >> (8 * i)) & 0xFF); outStream.Write(properties, 0, kPropSize); } UInt32[] tempPrices = new UInt32[Base.kNumFullDistances]; UInt32 _matchPriceCount; void FillDistancesPrices() { for (UInt32 i = Base.kStartPosModelIndex; i < Base.kNumFullDistances; i++) { UInt32 posSlot = GetPosSlot(i); int footerBits = (int)((posSlot >> 1) - 1); UInt32 baseVal = ((2 | (posSlot & 1)) << footerBits); tempPrices[i] = BitTreeEncoder.ReverseGetPrice(_posEncoders, baseVal - posSlot - 1, footerBits, i - baseVal); } for (UInt32 lenToPosState = 0; lenToPosState < Base.kNumLenToPosStates; lenToPosState++) { UInt32 posSlot; RangeCoder.BitTreeEncoder encoder = _posSlotEncoder[lenToPosState]; UInt32 st = (lenToPosState << Base.kNumPosSlotBits); for (posSlot = 0; posSlot < _distTableSize; posSlot++) _posSlotPrices[st + posSlot] = encoder.GetPrice(posSlot); for (posSlot = Base.kEndPosModelIndex; posSlot < _distTableSize; posSlot++) _posSlotPrices[st + posSlot] += ((((posSlot >> 1) - 1) - Base.kNumAlignBits) << RangeCoder.BitEncoder.kNumBitPriceShiftBits); UInt32 st2 = lenToPosState * Base.kNumFullDistances; UInt32 i; for (i = 0; i < Base.kStartPosModelIndex; i++) _distancesPrices[st2 + i] = _posSlotPrices[st + i]; for (; i < Base.kNumFullDistances; i++) _distancesPrices[st2 + i] = _posSlotPrices[st + GetPosSlot(i)] + tempPrices[i]; } _matchPriceCount = 0; } void FillAlignPrices() { for (UInt32 i = 0; i < Base.kAlignTableSize; i++) _alignPrices[i] = _posAlignEncoder.ReverseGetPrice(i); _alignPriceCount = 0; } static string[] kMatchFinderIDs = { "BT2", "BT4", }; static int FindMatchFinder(string s) { for (int m = 0; m < kMatchFinderIDs.Length; m++) if (s == kMatchFinderIDs[m]) return m; return -1; } public void SetCoderProperties(CoderPropID[] propIDs, object[] properties) { for (UInt32 i = 0; i < properties.Length; i++) { object prop = properties[i]; switch (propIDs[i]) { case CoderPropID.NumFastBytes: { if (!(prop is Int32)) throw new InvalidParamException(); Int32 numFastBytes = (Int32)prop; if (numFastBytes < 5 || numFastBytes > Base.kMatchMaxLen) throw new InvalidParamException(); _numFastBytes = (UInt32)numFastBytes; break; } case CoderPropID.Algorithm: { /* if (!(prop is Int32)) throw new InvalidParamException(); Int32 maximize = (Int32)prop; _fastMode = (maximize == 0); _maxMode = (maximize >= 2); */ break; } case CoderPropID.MatchFinder: { if (!(prop is String)) throw new InvalidParamException(); EMatchFinderType matchFinderIndexPrev = _matchFinderType; int m = FindMatchFinder(((string)prop).ToUpper()); if (m < 0) throw new InvalidParamException(); _matchFinderType = (EMatchFinderType)m; if (_matchFinder != null && matchFinderIndexPrev != _matchFinderType) { _dictionarySizePrev = 0xFFFFFFFF; _matchFinder = null; } break; } case CoderPropID.DictionarySize: { const int kDicLogSizeMaxCompress = 30; if (!(prop is Int32)) throw new InvalidParamException(); ; Int32 dictionarySize = (Int32)prop; if (dictionarySize < (UInt32)(1 << Base.kDicLogSizeMin) || dictionarySize > (UInt32)(1 << kDicLogSizeMaxCompress)) throw new InvalidParamException(); _dictionarySize = (UInt32)dictionarySize; int dicLogSize; for (dicLogSize = 0; dicLogSize < (UInt32)kDicLogSizeMaxCompress; dicLogSize++) if (dictionarySize <= ((UInt32)(1) << dicLogSize)) break; _distTableSize = (UInt32)dicLogSize * 2; break; } case CoderPropID.PosStateBits: { if (!(prop is Int32)) throw new InvalidParamException(); Int32 v = (Int32)prop; if (v < 0 || v > (UInt32)Base.kNumPosStatesBitsEncodingMax) throw new InvalidParamException(); _posStateBits = (int)v; _posStateMask = (((UInt32)1) << (int)_posStateBits) - 1; break; } case CoderPropID.LitPosBits: { if (!(prop is Int32)) throw new InvalidParamException(); Int32 v = (Int32)prop; if (v < 0 || v > (UInt32)Base.kNumLitPosStatesBitsEncodingMax) throw new InvalidParamException(); _numLiteralPosStateBits = (int)v; break; } case CoderPropID.LitContextBits: { if (!(prop is Int32)) throw new InvalidParamException(); Int32 v = (Int32)prop; if (v < 0 || v > (UInt32)Base.kNumLitContextBitsMax) throw new InvalidParamException(); ; _numLiteralContextBits = (int)v; break; } case CoderPropID.EndMarker: { if (!(prop is Boolean)) throw new InvalidParamException(); SetWriteEndMarkerMode((Boolean)prop); break; } default: throw new InvalidParamException(); } } } uint _trainSize = 0; public void SetTrainSize(uint trainSize) { _trainSize = trainSize; } } } ================================================ FILE: ClientUpdater/Compression/RangeCoder/RangeCoder.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ using System; namespace SevenZip.Compression.RangeCoder { class Encoder { public const uint kTopValue = (1 << 24); System.IO.Stream Stream; public UInt64 Low; public uint Range; uint _cacheSize; byte _cache; long StartPosition; public void SetStream(System.IO.Stream stream) { Stream = stream; } public void ReleaseStream() { Stream = null; } public void Init() { StartPosition = Stream.Position; Low = 0; Range = 0xFFFFFFFF; _cacheSize = 1; _cache = 0; } public void FlushData() { for (int i = 0; i < 5; i++) ShiftLow(); } public void FlushStream() { Stream.Flush(); } public void CloseStream() { Stream.Close(); } public void Encode(uint start, uint size, uint total) { Low += start * (Range /= total); Range *= size; while (Range < kTopValue) { Range <<= 8; ShiftLow(); } } public void ShiftLow() { if ((uint)Low < (uint)0xFF000000 || (uint)(Low >> 32) == 1) { byte temp = _cache; do { Stream.WriteByte((byte)(temp + (Low >> 32))); temp = 0xFF; } while (--_cacheSize != 0); _cache = (byte)(((uint)Low) >> 24); } _cacheSize++; Low = ((uint)Low) << 8; } public void EncodeDirectBits(uint v, int numTotalBits) { for (int i = numTotalBits - 1; i >= 0; i--) { Range >>= 1; if (((v >> i) & 1) == 1) Low += Range; if (Range < kTopValue) { Range <<= 8; ShiftLow(); } } } public void EncodeBit(uint size0, int numTotalBits, uint symbol) { uint newBound = (Range >> numTotalBits) * size0; if (symbol == 0) Range = newBound; else { Low += newBound; Range -= newBound; } while (Range < kTopValue) { Range <<= 8; ShiftLow(); } } public long GetProcessedSizeAdd() { return _cacheSize + Stream.Position - StartPosition + 4; // (long)Stream.GetProcessedSize(); } } class Decoder { public const uint kTopValue = (1 << 24); public uint Range; public uint Code; // public Buffer.InBuffer Stream = new Buffer.InBuffer(1 << 16); public System.IO.Stream Stream; public void Init(System.IO.Stream stream) { // Stream.Init(stream); Stream = stream; Code = 0; Range = 0xFFFFFFFF; for (int i = 0; i < 5; i++) Code = (Code << 8) | (byte)Stream.ReadByte(); } public void ReleaseStream() { // Stream.ReleaseStream(); Stream = null; } public void CloseStream() { Stream.Close(); } public void Normalize() { while (Range < kTopValue) { Code = (Code << 8) | (byte)Stream.ReadByte(); Range <<= 8; } } public void Normalize2() { if (Range < kTopValue) { Code = (Code << 8) | (byte)Stream.ReadByte(); Range <<= 8; } } public uint GetThreshold(uint total) { return Code / (Range /= total); } public void Decode(uint start, uint size, uint total) { Code -= start * Range; Range *= size; Normalize(); } public uint DecodeDirectBits(int numTotalBits) { uint range = Range; uint code = Code; uint result = 0; for (int i = numTotalBits; i > 0; i--) { range >>= 1; /* result <<= 1; if (code >= range) { code -= range; result |= 1; } */ uint t = (code - range) >> 31; code -= range & (t - 1); result = (result << 1) | (1 - t); if (range < kTopValue) { code = (code << 8) | (byte)Stream.ReadByte(); range <<= 8; } } Range = range; Code = code; return result; } public uint DecodeBit(uint size0, int numTotalBits) { uint newBound = (Range >> numTotalBits) * size0; uint symbol; if (Code < newBound) { symbol = 0; Range = newBound; } else { symbol = 1; Code -= newBound; Range -= newBound; } Normalize(); return symbol; } // ulong GetProcessedSize() {return Stream.GetProcessedSize(); } } } ================================================ FILE: ClientUpdater/Compression/RangeCoder/RangeCoderBit.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ using System; namespace SevenZip.Compression.RangeCoder { struct BitEncoder { public const int kNumBitModelTotalBits = 11; public const uint kBitModelTotal = (1 << kNumBitModelTotalBits); const int kNumMoveBits = 5; const int kNumMoveReducingBits = 2; public const int kNumBitPriceShiftBits = 6; uint Prob; public void Init() { Prob = kBitModelTotal >> 1; } public void UpdateModel(uint symbol) { if (symbol == 0) Prob += (kBitModelTotal - Prob) >> kNumMoveBits; else Prob -= (Prob) >> kNumMoveBits; } public void Encode(Encoder encoder, uint symbol) { // encoder.EncodeBit(Prob, kNumBitModelTotalBits, symbol); // UpdateModel(symbol); uint newBound = (encoder.Range >> kNumBitModelTotalBits) * Prob; if (symbol == 0) { encoder.Range = newBound; Prob += (kBitModelTotal - Prob) >> kNumMoveBits; } else { encoder.Low += newBound; encoder.Range -= newBound; Prob -= (Prob) >> kNumMoveBits; } if (encoder.Range < Encoder.kTopValue) { encoder.Range <<= 8; encoder.ShiftLow(); } } private static UInt32[] ProbPrices = new UInt32[kBitModelTotal >> kNumMoveReducingBits]; static BitEncoder() { const int kNumBits = (kNumBitModelTotalBits - kNumMoveReducingBits); for (int i = kNumBits - 1; i >= 0; i--) { UInt32 start = (UInt32)1 << (kNumBits - i - 1); UInt32 end = (UInt32)1 << (kNumBits - i); for (UInt32 j = start; j < end; j++) ProbPrices[j] = ((UInt32)i << kNumBitPriceShiftBits) + (((end - j) << kNumBitPriceShiftBits) >> (kNumBits - i - 1)); } } public uint GetPrice(uint symbol) { return ProbPrices[(((Prob - symbol) ^ ((-(int)symbol))) & (kBitModelTotal - 1)) >> kNumMoveReducingBits]; } public uint GetPrice0() { return ProbPrices[Prob >> kNumMoveReducingBits]; } public uint GetPrice1() { return ProbPrices[(kBitModelTotal - Prob) >> kNumMoveReducingBits]; } } struct BitDecoder { public const int kNumBitModelTotalBits = 11; public const uint kBitModelTotal = (1 << kNumBitModelTotalBits); const int kNumMoveBits = 5; uint Prob; public void UpdateModel(int numMoveBits, uint symbol) { if (symbol == 0) Prob += (kBitModelTotal - Prob) >> numMoveBits; else Prob -= (Prob) >> numMoveBits; } public void Init() { Prob = kBitModelTotal >> 1; } public uint Decode(RangeCoder.Decoder rangeDecoder) { uint newBound = (uint)(rangeDecoder.Range >> kNumBitModelTotalBits) * (uint)Prob; if (rangeDecoder.Code < newBound) { rangeDecoder.Range = newBound; Prob += (kBitModelTotal - Prob) >> kNumMoveBits; if (rangeDecoder.Range < Decoder.kTopValue) { rangeDecoder.Code = (rangeDecoder.Code << 8) | (byte)rangeDecoder.Stream.ReadByte(); rangeDecoder.Range <<= 8; } return 0; } else { rangeDecoder.Range -= newBound; rangeDecoder.Code -= newBound; Prob -= (Prob) >> kNumMoveBits; if (rangeDecoder.Range < Decoder.kTopValue) { rangeDecoder.Code = (rangeDecoder.Code << 8) | (byte)rangeDecoder.Stream.ReadByte(); rangeDecoder.Range <<= 8; } return 1; } } } } ================================================ FILE: ClientUpdater/Compression/RangeCoder/RangeCoderBitTree.cs ================================================ // ------------------------------------------------------------------------------ // // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ using System; namespace SevenZip.Compression.RangeCoder { struct BitTreeEncoder { BitEncoder[] Models; int NumBitLevels; public BitTreeEncoder(int numBitLevels) { NumBitLevels = numBitLevels; Models = new BitEncoder[1 << numBitLevels]; } public void Init() { for (uint i = 1; i < (1 << NumBitLevels); i++) Models[i].Init(); } public void Encode(Encoder rangeEncoder, UInt32 symbol) { UInt32 m = 1; for (int bitIndex = NumBitLevels; bitIndex > 0; ) { bitIndex--; UInt32 bit = (symbol >> bitIndex) & 1; Models[m].Encode(rangeEncoder, bit); m = (m << 1) | bit; } } public void ReverseEncode(Encoder rangeEncoder, UInt32 symbol) { UInt32 m = 1; for (UInt32 i = 0; i < NumBitLevels; i++) { UInt32 bit = symbol & 1; Models[m].Encode(rangeEncoder, bit); m = (m << 1) | bit; symbol >>= 1; } } public UInt32 GetPrice(UInt32 symbol) { UInt32 price = 0; UInt32 m = 1; for (int bitIndex = NumBitLevels; bitIndex > 0; ) { bitIndex--; UInt32 bit = (symbol >> bitIndex) & 1; price += Models[m].GetPrice(bit); m = (m << 1) + bit; } return price; } public UInt32 ReverseGetPrice(UInt32 symbol) { UInt32 price = 0; UInt32 m = 1; for (int i = NumBitLevels; i > 0; i--) { UInt32 bit = symbol & 1; symbol >>= 1; price += Models[m].GetPrice(bit); m = (m << 1) | bit; } return price; } public static UInt32 ReverseGetPrice(BitEncoder[] Models, UInt32 startIndex, int NumBitLevels, UInt32 symbol) { UInt32 price = 0; UInt32 m = 1; for (int i = NumBitLevels; i > 0; i--) { UInt32 bit = symbol & 1; symbol >>= 1; price += Models[startIndex + m].GetPrice(bit); m = (m << 1) | bit; } return price; } public static void ReverseEncode(BitEncoder[] Models, UInt32 startIndex, Encoder rangeEncoder, int NumBitLevels, UInt32 symbol) { UInt32 m = 1; for (int i = 0; i < NumBitLevels; i++) { UInt32 bit = symbol & 1; Models[startIndex + m].Encode(rangeEncoder, bit); m = (m << 1) | bit; symbol >>= 1; } } } struct BitTreeDecoder { BitDecoder[] Models; int NumBitLevels; public BitTreeDecoder(int numBitLevels) { NumBitLevels = numBitLevels; Models = new BitDecoder[1 << numBitLevels]; } public void Init() { for (uint i = 1; i < (1 << NumBitLevels); i++) Models[i].Init(); } public uint Decode(RangeCoder.Decoder rangeDecoder) { uint m = 1; for (int bitIndex = NumBitLevels; bitIndex > 0; bitIndex--) m = (m << 1) + Models[m].Decode(rangeDecoder); return m - ((uint)1 << NumBitLevels); } public uint ReverseDecode(RangeCoder.Decoder rangeDecoder) { uint m = 1; uint symbol = 0; for (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++) { uint bit = Models[m].Decode(rangeDecoder); m <<= 1; m += bit; symbol |= (bit << bitIndex); } return symbol; } public static uint ReverseDecode(BitDecoder[] Models, UInt32 startIndex, RangeCoder.Decoder rangeDecoder, int NumBitLevels) { uint m = 1; uint symbol = 0; for (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++) { uint bit = Models[startIndex + m].Decode(rangeDecoder); m <<= 1; m += bit; symbol |= (bit << bitIndex); } return symbol; } } } ================================================ FILE: ClientUpdater/CustomComponent.cs ================================================ // Copyright 2022-2025 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . namespace ClientUpdater; using ClientCore.Extensions; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Handlers; using System.Threading; using System.Threading.Tasks; using ClientUpdater.Compression; using Rampastring.Tools; /// /// Custom component. /// public class CustomComponent { /// /// UI name of custom component. /// public string GUIName { get; internal set; } /// /// INI name of custom component. /// public string ININame { get; internal set; } /// /// Local file system path of custom component. /// public string LocalPath { get; internal set; } /// /// Download path of custom component. /// public string DownloadPath { get; internal set; } /// /// Is download path treated as an absolute URL? /// public bool IsDownloadPathAbsolute { get; internal set; } /// /// If set, no archive extension is used for download file path. /// public bool NoArchiveExtensionForDownloadPath { get; internal set; } /// /// Is this custom component currently being downloaded? /// public bool IsBeingDownloaded { get; internal set; } /// /// File identifier from local version file. /// public string LocalIdentifier { get; internal set; } /// /// File identifier from server version file. /// public string RemoteIdentifier { get; internal set; } /// /// File size from server version file. /// public long RemoteSize { get; internal set; } /// /// Archive file size from server version file. /// public long RemoteArchiveSize { get; internal set; } /// /// Is custom component an archived file? /// public bool Archived { get; internal set; } /// /// Has custom component been initialized? /// public bool Initialized { get; internal set; } private readonly List filesToCleanup = new(); private int currentDownloadPercentage; private CancellationTokenSource downloadTaskCancelTokenSource; private CancellationToken downloadTaskCancelToken; /// /// Creates new custom component. /// public CustomComponent() { } /// /// Creates new custom component from given information. /// public CustomComponent(string guiName, string iniName, string downloadPath, string localPath, bool isDownloadPathAbsolute = false, bool noArchiveExtensionForDownloadPath = false) { GUIName = guiName.L10N($"INI:CustomComponents:{iniName}:UIName"); ININame = iniName; LocalPath = localPath; DownloadPath = downloadPath; IsDownloadPathAbsolute = isDownloadPathAbsolute; NoArchiveExtensionForDownloadPath = noArchiveExtensionForDownloadPath; } /// /// Starts download for this custom component. /// public void DownloadComponent() { downloadTaskCancelTokenSource ??= new(); downloadTaskCancelToken = downloadTaskCancelTokenSource.Token; #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed DoDownloadComponentAsync(downloadTaskCancelToken); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } /// /// Stops downloading of this custom component. /// public void StopDownload() { if (downloadTaskCancelTokenSource is { IsCancellationRequested: false }) downloadTaskCancelTokenSource.Cancel(); } /// /// Handles downloading of the custom component. /// private async Task DoDownloadComponentAsync(CancellationToken cancellationToken) { ProgressMessageHandler progressMessageHandler = null; try { Logger.Log("CustomComponent: Initializing download of custom component: " + GUIName); #if NETFRAMEWORK progressMessageHandler = new(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, SslProtocols = System.Security.Authentication.SslProtocols.Tls | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13, }); using var httpClient = new HttpClient(progressMessageHandler, true); #else progressMessageHandler = new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(15), AutomaticDecompression = DecompressionMethods.All }); using var httpClient = new HttpClient(progressMessageHandler, true) { DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; #endif IsBeingDownloaded = true; currentDownloadPercentage = -1; string uniqueIdForFile; string uriString = Updater.CurrentUpdateServerURL + Updater.VERSION_FILE; string finalFileName = SafePath.CombineFilePath(Updater.GamePath, LocalPath); string finalFileNameTemp = FormattableString.Invariant($"{finalFileName}_u"); string versionFileName = SafePath.CombineFilePath(Updater.GamePath, FormattableString.Invariant($"{Updater.VERSION_FILE}_cc")); Updater.CreatePath(finalFileName); Updater.UpdateUserAgent(httpClient); progressMessageHandler.HttpReceiveProgress += ProgressMessageHandlerOnHttpReceiveProgress; Logger.Log("CustomComponent: Downloading version info."); var versionFileStream = new FileStream(versionFileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (versionFileStream) { Stream stream = await httpClient.GetStreamAsync(new Uri(uriString)).ConfigureAwait(false); using (stream) { await stream.CopyToAsync(versionFileStream, 81920, cancellationToken).ConfigureAwait(false); } } var version = new IniFile(versionFileName); string[] tmp = version.GetStringListValue("AddOns", ININame, string.Empty); Updater.GetArchiveInfo(version, LocalPath, out string archiveID, out int archiveSize); UpdaterFileInfo info = Updater.CreateFileInfo(finalFileName, tmp[0], Conversions.IntFromString(tmp[1], 0), archiveID, archiveSize); Logger.Log("CustomComponent: Version info parsed. Proceeding to download component."); int num = 0; Uri downloadUri = GetDownloadUri(DownloadPath, info); string downloadFileName = FormattableString.Invariant($"{GetArchivePath(finalFileName, info)}_u"); Logger.Log("CustomComponent: Download URL for custom component " + GUIName + ": " + downloadUri.AbsoluteUri); while (true) { filesToCleanup.Clear(); filesToCleanup.Add(versionFileName); filesToCleanup.Add(downloadFileName); num++; var downloadFileStream = new FileStream(downloadFileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (downloadFileStream) { Stream stream = await httpClient.GetStreamAsync(downloadUri).ConfigureAwait(false); using (stream) { await stream.CopyToAsync(downloadFileStream, 81920, cancellationToken).ConfigureAwait(false); } } Logger.Log("CustomComponent: Download of custom component " + GUIName + " finished - verifying."); if (info.Archived) { filesToCleanup.Add(finalFileNameTemp); string archiveLocalPath = GetArchivePath(LocalPath, info); string archiveLocalPathTemp = FormattableString.Invariant($"{archiveLocalPath}_u"); FileInfo archivePathFileInfo = SafePath.GetFile(Updater.GamePath, archiveLocalPathTemp); Logger.Log("CustomComponent: Custom component is an archive."); string archiveIdentifier = Updater.GetUniqueIdForFile(archiveLocalPathTemp); if (archiveIdentifier != info.ArchiveIdentifier) { cancellationToken.ThrowIfCancellationRequested(); if (num > 2) throw new("Too many retries for downloading component."); Logger.Log("CustomComponent: Downloaded archive " + archiveLocalPath + "_u has a non-matching identifier: " + archiveIdentifier + " against " + info.ArchiveIdentifier + ". Retrying."); Updater.DeleteFileAndWait(archivePathFileInfo.FullName); continue; } cancellationToken.ThrowIfCancellationRequested(); Logger.Log("CustomComponent: Archive " + archiveLocalPath + "_u is intact. Unpacking..."); await CompressionHelper.DecompressFileAsync(archivePathFileInfo.FullName, finalFileNameTemp, downloadTaskCancelToken).ConfigureAwait(false); archivePathFileInfo.Delete(); } cancellationToken.ThrowIfCancellationRequested(); uniqueIdForFile = Updater.GetUniqueIdForFile(FormattableString.Invariant($"{LocalPath}_u")); if (info.Identifier != uniqueIdForFile) { if (num > 2) throw new("Too many retries for downloading component."); cancellationToken.ThrowIfCancellationRequested(); Logger.Log("CustomComponent: Incorrect custom component identifier for " + GUIName + ": " + uniqueIdForFile + " against " + info.Identifier + ". Retrying."); continue; } break; } cancellationToken.ThrowIfCancellationRequested(); Logger.Log("Downloaded custom component " + GUIName + " verified successfully."); File.Copy(finalFileNameTemp, finalFileName, true); LocalIdentifier = uniqueIdForFile; IsBeingDownloaded = false; CleanUpAfterDownload(); DoDownloadFinished(true); } catch (Exception e) { if (e is AggregateException) { bool canceled = false; bool displayError = false; foreach (Exception ei in (e as AggregateException).InnerExceptions) { if (ei is TaskCanceledException or OperationCanceledException) { canceled = true; } else { if (!displayError) { Logger.Log("CustomComponent: One or more errors occurred while downloading custom component " + GUIName + ". The download has been aborted."); displayError = true; } Logger.Log("Message: " + ei.Message); } if (canceled) { HandleAfterCancelDownload(); } else { IsBeingDownloaded = false; CleanUpAfterDownload(); DoDownloadFinished(false); } } return; } if (e is TaskCanceledException or OperationCanceledException) { HandleAfterCancelDownload(); return; } Logger.Log("CustomComponent: An error occurred while downloading custom component " + GUIName + ". The download has been aborted. Message: " + e.Message); IsBeingDownloaded = false; CleanUpAfterDownload(); DoDownloadFinished(false); } finally { downloadTaskCancelTokenSource.Dispose(); downloadTaskCancelTokenSource = null; progressMessageHandler.HttpReceiveProgress -= ProgressMessageHandlerOnHttpReceiveProgress; } } private void HandleAfterCancelDownload() { Logger.Log("CustomComponent: Download of custom component " + GUIName + " canceled."); IsBeingDownloaded = false; DoDownloadFinished(false); CleanUpAfterDownload(); } private Uri GetDownloadUri(string downloadPath, UpdaterFileInfo info) { string fullPath; if (!IsDownloadPathAbsolute) fullPath = Updater.CurrentUpdateServerURL + downloadPath; else fullPath = downloadPath; return new(NoArchiveExtensionForDownloadPath ? fullPath : GetArchivePath(fullPath, info)); } private static string GetArchivePath(string path, UpdaterFileInfo info) { if (info.Archived) return path + Updater.ARCHIVE_FILE_EXTENSION; return path; } private void CleanUpAfterDownload() { try { foreach (string filename in filesToCleanup) { if (File.Exists(filename)) { new FileInfo(filename).IsReadOnly = false; File.Delete(filename); } } } catch (Exception) { } } public event DownloadFinishedEventHandler DownloadFinished; public event DownloadProgressChangedEventHandler DownloadProgressChanged; public delegate void DownloadFinishedEventHandler(CustomComponent cc, bool success); public delegate void DownloadProgressChangedEventHandler(CustomComponent cc, int percentage); private void DoDownloadFinished(bool success) => DownloadFinished?.Invoke(this, success); private void ProgressMessageHandlerOnHttpReceiveProgress(object sender, HttpProgressEventArgs e) { if (e.ProgressPercentage != currentDownloadPercentage) { currentDownloadPercentage = e.ProgressPercentage; DownloadProgressChanged?.Invoke(this, currentDownloadPercentage); } } } ================================================ FILE: ClientUpdater/UpdateMirror.cs ================================================ // Copyright 2022-2024 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . namespace ClientUpdater; /// /// Update mirror info. /// public readonly record struct UpdateMirror(string URL, string Name, string Location); ================================================ FILE: ClientUpdater/Updater.cs ================================================ // Copyright 2022-2025 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . namespace ClientUpdater; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Handlers; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using ClientUpdater.Compression; using ClientCore.Extensions; using Rampastring.Tools; public static class Updater { #if NETFRAMEWORK private const string SECOND_STAGE_UPDATER = "SecondStageUpdater.exe"; #else private const string SECOND_STAGE_UPDATER = "SecondStageUpdater.dll"; #endif private const string LEGACY_SECOND_STAGE_UPDATER = "clientupdt.dat"; public const string VERSION_FILE = "version"; public const string ARCHIVE_FILE_EXTENSION = ".lzma"; #if NETFRAMEWORK private const string BINARIES_FOLDER = "Binaries"; #else private const string BINARIES_FOLDER = "BinariesNET8"; #endif /// /// Currently set game path for the updater. /// public static string GamePath { get; private set; } = string.Empty; /// /// Currently set resource path for the updater. /// public static string ResourcePath { get; private set; } = string.Empty; /// /// Currently set local game ID for the updater. /// public static string LocalGame { get; private set; } = "None"; /// /// Currently set calling executable file name for the updater. /// public static string CallingExecutableFileName { get; private set; } = string.Empty; /// /// Gets read-only collection of all custom components. /// public static ReadOnlyCollection CustomComponents => customComponents?.AsReadOnly(); /// /// Gets read-only collection of all update mirrors. /// public static ReadOnlyCollection UpdateMirrors => updateMirrors?.AsReadOnly(); /// /// Update server URL for current update mirror if available. /// public static string CurrentUpdateServerURL => updateMirrors is { Count: > 0 } ? updateMirrors[currentUpdateMirrorIndex].URL : null; private static VersionState _versionState = VersionState.UNKNOWN; /// /// Current version state of the updater. /// public static VersionState VersionState { get => _versionState; private set { _versionState = value; DoOnVersionStateChanged(); } } /// /// Does the currently available update (if applicable) require manual download? /// public static bool ManualUpdateRequired { get; private set; } /// /// Manual download URL for currently available update, if available. /// public static string ManualDownloadURL { get; private set; } = string.Empty; /// /// Local version file updater version. /// public static string UpdaterVersion { get; private set; } = "N/A"; /// /// Local version file game version. /// public static string GameVersion { get; private set; } = "N/A"; /// /// Server version file game version. /// public static string ServerGameVersion { get; private set; } = "N/A"; /// /// Size of current update in kilobytes. /// public static int UpdateSizeInKb { get; private set; } // Misc. private static int currentUpdateMirrorIndex; private static IniFile settingsINI; private static List customComponents; private static List updateMirrors; private static string[] ignoreMasks = [".rtf", ".txt", "Theme.ini", "gui_settings.xml"]; // File infos. private static readonly List FileInfosToDownload = new(); private static readonly List ServerFileInfos = new(); private static readonly List LocalFileInfos = new(); #if NETFRAMEWORK private static readonly Lazy lazySharedProgressMessageHandler = new Lazy(() => new(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, SslProtocols = System.Security.Authentication.SslProtocols.Tls | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13, }), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy lazySharedHttpClient = new Lazy(() => new(SharedProgressMessageHandler, true), LazyThreadSafetyMode.ExecutionAndPublication); #else private static readonly Lazy lazySharedProgressMessageHandler = new Lazy(() => new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(15), AutomaticDecompression = DecompressionMethods.All }), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy lazySharedHttpClient = new Lazy(() => new HttpClient(SharedProgressMessageHandler, true) { DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }, LazyThreadSafetyMode.ExecutionAndPublication); #endif private static readonly ProgressMessageHandler SharedProgressMessageHandler = lazySharedProgressMessageHandler.Value; private static readonly HttpClient SharedHttpClient = lazySharedHttpClient.Value; // Current update / download related. private static bool terminateUpdate; private static string currentFilename; private static int currentFileSize; private static int totalDownloadedKbs; /// /// Initializes the updater. /// /// Path of the root client / game folder. /// Path of the resource folder of client / game. /// Client settings INI filename. /// Local game ID of the current game. /// File name of the calling executable. public static void Initialize(string gamePath, string resourcePath, string settingsIniName, string localGame, string callingExecutableFileName) { Logger.Log("Updater: Initializing updater."); GamePath = gamePath; ResourcePath = resourcePath; settingsINI = new(SafePath.CombineFilePath(GamePath, settingsIniName)); LocalGame = localGame; CallingExecutableFileName = callingExecutableFileName; ReadUpdaterConfig(); Logger.Log("Updater: Update mirror count: " + updateMirrors.Count); Logger.Log("Updater: Running from: " + CallingExecutableFileName); var list = new List(); List sectionKeys = settingsINI.GetSectionKeys("DownloadMirrors"); if (sectionKeys != null) { foreach (string str in sectionKeys) { string value = settingsINI.GetStringValue("DownloadMirrors", str, string.Empty); if (updateMirrors.Any(um => value.Equals(um.Name, StringComparison.OrdinalIgnoreCase))) { UpdateMirror item = updateMirrors.Single(um => value.Equals(um.Name, StringComparison.OrdinalIgnoreCase)); if (!list.Contains(item)) list.Add(item); } } } foreach (UpdateMirror mirror2 in updateMirrors) { if (!list.Contains(mirror2)) list.Add(mirror2); } updateMirrors = list; } /// /// Checks if there are available updates. /// public static void CheckForUpdates() { Logger.Log("Updater: Checking for updates."); if (VersionState is not VersionState.UPDATECHECKINPROGRESS and not VersionState.UPDATEINPROGRESS) #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed DoVersionCheckAsync(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } /// /// Checks version information of local files. /// public static void CheckLocalFileVersions() { Logger.Log("Updater: Checking local file versions."); LocalFileInfos.Clear(); var file = new IniFile(SafePath.CombineFilePath(GamePath, VERSION_FILE)); GameVersion = file.GetStringValue("DTA", "Version", "N/A"); UpdaterVersion = file.GetStringValue("DTA", "UpdaterVersion", "N/A"); List sectionKeys = file.GetSectionKeys("FileVersions"); if (sectionKeys != null) { char[] separator = new char[] { ',' }; foreach (string str in sectionKeys) { string[] strArray = file.GetStringListValue("FileVersions", str, string.Empty, separator); string[] strArrayArch = file.GetStringListValue("ArchivedFiles", str, string.Empty, separator); bool archiveAvailable = strArrayArch is { Length: >= 2 }; if (strArray.Length >= 2) { var item = new UpdaterFileInfo( SafePath.CombineFilePath(str), Conversions.IntFromString(strArray[1], 0)) { Identifier = strArray[0], ArchiveIdentifier = archiveAvailable ? strArrayArch[0] : string.Empty, ArchiveSize = archiveAvailable ? Conversions.IntFromString(strArrayArch[1], 0) : 0 }; LocalFileInfos.Add(item); } else { Logger.Log("Updater: Warning: Malformed file info in local version information: " + str); } } } OnLocalFileVersionsChecked?.Invoke(); } /// /// Starts update process. /// #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed public static void StartUpdate() => PerformUpdateAsync(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed /// /// Stops current update process. /// public static void StopUpdate() => terminateUpdate = true; /// /// Clears current version file information. /// public static void ClearVersionInfo() { LocalFileInfos.Clear(); ServerFileInfos.Clear(); FileInfosToDownload.Clear(); GameVersion = "N/A"; VersionState = VersionState.UNKNOWN; } /// /// Checks if file /// public static bool IsFileNonexistantOrOriginal(string filePath) { UpdaterFileInfo info = LocalFileInfos.Find(f => f.Filename.Equals(filePath, StringComparison.OrdinalIgnoreCase)); if (info == null) return true; string uniqueIdForFile = GetUniqueIdForFile(info.Filename); return info.Identifier == uniqueIdForFile; } /// /// Moves update mirror down in list of update mirrors. /// /// Index of mirror to move in the list. public static void MoveMirrorDown(int mirrorIndex) { if (mirrorIndex > updateMirrors.Count - 2 || mirrorIndex < 0) return; (updateMirrors[mirrorIndex], updateMirrors[mirrorIndex + 1]) = (updateMirrors[mirrorIndex + 1], updateMirrors[mirrorIndex]); } /// /// Moves update mirror up in list of update mirrors. /// /// Index of mirror to move in the list. public static void MoveMirrorUp(int mirrorIndex) { if (updateMirrors.Count <= mirrorIndex || mirrorIndex < 1) return; (updateMirrors[mirrorIndex], updateMirrors[mirrorIndex - 1]) = (updateMirrors[mirrorIndex - 1], updateMirrors[mirrorIndex]); } /// /// Returns whether or not there is a currently active custom component in progress. /// /// True if custom component download is in progress, otherwise false. public static bool IsComponentDownloadInProgress() { if (customComponents == null) return false; return customComponents.Any(c => c.IsBeingDownloaded); } /// /// Gets custom component index based on name. /// /// Name of custom component. /// Component index if found, otherwise -1. public static int GetComponentIndex(string componentName) { if (customComponents == null) return -1; return customComponents.FindIndex(c => c.ININame == componentName); } /// /// Get archive info for a file from version file. /// /// Version file. /// Filename. /// Set to archive ID. /// Set to archive file size. internal static void GetArchiveInfo(IniFile versionFile, string filename, out string archiveID, out int archiveSize) { string[] values = versionFile.GetStringValue("ArchivedFiles", filename, string.Empty).Split(','); bool archiveAvailable = values is { Length: >= 2 }; archiveID = archiveAvailable ? values[0] : string.Empty; archiveSize = archiveAvailable ? Conversions.IntFromString(values[1], 0) : 0; } /// /// Creates file info instance from given information. /// /// Filename. /// File identifier. /// File size. /// Archive file identifier. /// Archive file size. internal static UpdaterFileInfo CreateFileInfo(string filename, string identifier, int size, string archiveIdentifier = null, int archiveSize = 0) { return new(SafePath.CombineFilePath(filename), size) { Identifier = identifier, ArchiveIdentifier = archiveIdentifier, ArchiveSize = archiveSize }; } internal static void UpdateUserAgent(HttpClient httpClient) { httpClient.DefaultRequestHeaders.UserAgent.Clear(); if (GameVersion != "N/A") httpClient.DefaultRequestHeaders.UserAgent.Add(new(LocalGame.Replace(' ', '-'), GameVersion.Replace(' ', '-'))); if (UpdaterVersion != "N/A") httpClient.DefaultRequestHeaders.UserAgent.Add(new(nameof(Updater), UpdaterVersion.Replace(' ', '-'))); httpClient.DefaultRequestHeaders.UserAgent.Add(new("Client", GitVersionInformation.AssemblySemVer)); } /// /// Deletes file and waits until it has been deleted. /// /// File to delete. /// Maximum time to wait in milliseconds. internal static void DeleteFileAndWait(string filepath, int timeout = 10000) { FileInfo fileInfo = SafePath.GetFile(filepath); using var fw = new FileSystemWatcher(fileInfo.DirectoryName, fileInfo.Name); using var mre = new ManualResetEventSlim(); fw.EnableRaisingEvents = true; fw.Deleted += (_, _) => { mre.Set(); }; if (fileInfo.Exists) fileInfo.IsReadOnly = false; fileInfo.Delete(); mre.Wait(timeout); } /// /// Creates all directories required for file path. /// /// File path. internal static void CreatePath(string filePath) { FileInfo fileInfo = SafePath.GetFile(filePath); if (!fileInfo.Directory.Exists) fileInfo.Directory.Create(); } internal static string GetUniqueIdForFile(string filePath) { using var md = MD5.Create(); md.Initialize(); using FileStream fs = SafePath.GetFile(GamePath, filePath).OpenRead(); md.ComputeHash(fs); var builder = new StringBuilder(); foreach (byte num2 in md.Hash) builder.Append(num2); md.Clear(); return builder.ToString(); } /// /// Parse updater configuration file. /// private static void ReadUpdaterConfig() { var mirrors = new List(); var customComponents = new List(); FileInfo configFile = SafePath.GetFile(ResourcePath, "UpdaterConfig.ini"); if (!configFile.Exists) { Logger.Log("Updater config file not found - attempting to read legacy updateconfig.ini."); ReadLegacyUpdaterConfig(mirrors); } else { var updaterConfig = new IniFile(configFile.FullName); string maskString = updaterConfig.GetStringValue("Settings", "IgnoreMasks", string.Join(",", ignoreMasks)); ignoreMasks = maskString.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); List keys = updaterConfig.GetSectionKeys("DownloadMirrors"); if (keys != null) { foreach (string key in keys) { if (string.IsNullOrEmpty(key)) continue; string stringValue = updaterConfig.GetStringValue("DownloadMirrors", key, string.Empty); if (string.IsNullOrEmpty(stringValue)) stringValue = key; string[] values = stringValue.Split(','); if (values.Length < 2) continue; string url = values[0].Trim().TrimEnd('/') + "/"; if (mirrors.FindIndex(i => i.URL == url) < 0) mirrors.Add(new(url, values[1].Trim(), values.Length > 2 ? values[2].Trim() : string.Empty)); } } keys = updaterConfig.GetSectionKeys("CustomComponents"); if (keys != null) { foreach (string key in keys) { if (string.IsNullOrEmpty(key)) continue; string stringValue = updaterConfig.GetStringValue("CustomComponents", key, string.Empty); if (string.IsNullOrEmpty(stringValue)) stringValue = key; string[] values = stringValue.Split(','); if (values.Length < 4) continue; string ID = values[1].Trim(); if (customComponents.FindIndex(i => i.ININame == ID) < 0) { string Name = values[0].Trim(); string DownloadPath = values[2].Trim(); string LocalPath = values[3].Trim(); bool noArchiveExtensionForDownloadPath = false; if (values.Length > 4) noArchiveExtensionForDownloadPath = Conversions.BooleanFromString(values[4], false); bool DownloadPathIsAbsolute = Uri.IsWellFormedUriString(DownloadPath, UriKind.Absolute); customComponents.Add(new(Name, ID, DownloadPath, LocalPath, DownloadPathIsAbsolute, noArchiveExtensionForDownloadPath)); } } } } updateMirrors = mirrors; Updater.customComponents = customComponents; if (updateMirrors.Count < 1) Logger.Log("Warning: No download mirrors found in updater config file or the built-in game info."); } /// /// Parse legacy format updater configuration file. /// /// List of update mirrors to add update mirrors to. private static void ReadLegacyUpdaterConfig(List updateMirrors) { FileInfo updateConfigFile = SafePath.GetFile(GamePath, "updateconfig.ini"); if (!updateConfigFile.Exists) return; string[] lines; try { lines = File.ReadAllLines(updateConfigFile.FullName); } catch (Exception e) { Logger.Log("Error: Could not read legacy format updateconfig.ini. Message:" + e.Message); return; } foreach (string line in lines) { if (string.IsNullOrWhiteSpace(line) || line.Trim().StartsWith(';')) continue; string[] array = line.Split(new char[] { ',' }); if (array.Length < 3) continue; string url = array[0].Trim().TrimEnd('/') + "/"; string name = array[1].Trim(); string location = array[2].Trim(); updateMirrors.Add(new(url, name, location)); } } /// /// Performs a version file check on update server. /// private static async Task DoVersionCheckAsync() { Logger.Log("Updater: Doing version file check."); ServerFileInfos.Clear(); FileInfosToDownload.Clear(); UpdateSizeInKb = 0; try { VersionState = VersionState.UPDATECHECKINPROGRESS; if (updateMirrors.Count == 0) { Logger.Log("Updater: There are no update mirrors!"); } else { Logger.Log("Updater: Checking version on the server."); UpdateUserAgent(SharedHttpClient); FileInfo versionFile = SafePath.GetFile(GamePath, FormattableString.Invariant($"{VERSION_FILE}_u")); while (currentUpdateMirrorIndex < updateMirrors.Count) { try { Logger.Log("Updater: Trying to connect to update mirror " + updateMirrors[currentUpdateMirrorIndex].URL); FileStream fileStream = new FileStream(versionFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (fileStream) { Stream stream = await SharedHttpClient.GetStreamAsync(updateMirrors[currentUpdateMirrorIndex].URL + VERSION_FILE).ConfigureAwait(false); using (stream) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } break; } catch (Exception e) { Logger.Log("Updater: Error connecting to update mirror. Error message: " + e.ToString()); Logger.Log("Updater: Seeking other mirrors..."); currentUpdateMirrorIndex++; if (currentUpdateMirrorIndex >= updateMirrors.Count) { currentUpdateMirrorIndex = 0; throw new("Unable to connect to update servers."); } } } Logger.Log("Updater: Downloaded version information."); var version = new IniFile(versionFile.FullName); string versionString = version.GetStringValue("DTA", "Version", string.Empty); string updaterVersionString = version.GetStringValue("DTA", "UpdaterVersion", "N/A"); string manualDownloadURLString = version.GetStringValue("DTA", "ManualDownloadURL", string.Empty); if (version.SectionExists("FileVersions")) { foreach (string key in version.GetSectionKeys("FileVersions")) { string[] tmp = version.GetStringValue("FileVersions", key, string.Empty).Split(','); if (tmp.Length < 2) { Logger.Log("Updater: Warning: Malformed file info in downloaded version information: " + key); continue; } GetArchiveInfo(version, key, out string archiveID, out int archiveSize); UpdaterFileInfo item = CreateFileInfo(key, tmp[0], Conversions.IntFromString(tmp[1], 0), archiveID, archiveSize); ServerFileInfos.Add(item); } } if (version.SectionExists("AddOns")) { foreach (string key in version.GetSectionKeys("AddOns")) { string[] tmp = version.GetStringValue("AddOns", key, string.Empty).Split(','); if (tmp.Length < 2) { Logger.Log("Updater: Warning: Malformed addon info in downloaded version information: " + key); continue; } UpdaterFileInfo item = CreateFileInfo(key, tmp[0], Conversions.IntFromString(tmp[1], 0), string.Empty, 0); int index = GetComponentIndex(key); if (index == -1) { Logger.Log("Updater: Warning: Invalid custom component ID " + key); } else { CustomComponent component = customComponents[index]; component.Initialized = false; Logger.Log("Updater: Setting custom component info for " + key); GetArchiveInfo(version, component.LocalPath, out string archiveID, out int archiveSize); item.ArchiveIdentifier = archiveID; item.ArchiveSize = archiveSize; component.RemoteSize = item.Size * 1024; component.RemoteArchiveSize = item.Archived ? item.ArchiveSize * 1024 : 0; component.RemoteIdentifier = item.Identifier; component.Archived = item.Archived; if (SafePath.GetFile(GamePath, component.LocalPath).Exists) component.LocalIdentifier = GetUniqueIdForFile(component.LocalPath); component.Initialized = true; } } } if (string.IsNullOrEmpty(versionString)) throw new("Update server integrity error while checking for updates."); Logger.Log("Updater: Server game version is " + versionString + ", local version is " + GameVersion); ServerGameVersion = versionString; if (versionString == GameVersion) { VersionState = VersionState.UPTODATE; versionFile.Delete(); DoFileIdentifiersUpdatedEvent(); if (AreCustomComponentsOutdated()) DoCustomComponentsOutdatedEvent(); } else { if (updaterVersionString != "N/A" && UpdaterVersion != updaterVersionString) { Logger.Log("Updater: Server update system version is set to " + updaterVersionString + " and is different to local update system version " + UpdaterVersion + ". Manual update required."); VersionState = VersionState.OUTDATED; ManualUpdateRequired = true; ManualDownloadURL = manualDownloadURLString; versionFile.Delete(); DoFileIdentifiersUpdatedEvent(); } else { VersionCheckHandle(); } } } } catch (Exception exception) { VersionState = VersionState.UNKNOWN; Logger.Log("Updater: An error occured while performing version check: " + exception.Message); DoFileIdentifiersUpdatedEvent(); } } /// /// Checks if custom components are outdated. /// /// True if custom components are outdated, otherwise false. private static bool AreCustomComponentsOutdated() { Logger.Log("Updater: Checking if custom components are outdated."); foreach (CustomComponent component in customComponents) { if (SafePath.GetFile(GamePath, component.LocalPath).Exists && component.RemoteIdentifier != component.LocalIdentifier) return true; } return false; } /// /// Executes after-update script file. /// private static async ValueTask ExecuteAfterUpdateScriptAsync() { Logger.Log("Updater: Downloading updateexec."); try { string downloadFile = SafePath.CombineFilePath(GamePath, "updateexec"); FileStream fileStream = new FileStream(downloadFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (fileStream) { Stream stream = await SharedHttpClient.GetStreamAsync(updateMirrors[currentUpdateMirrorIndex].URL + "updateexec").ConfigureAwait(false); using (stream) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } } catch (Exception exception) { Logger.Log("Updater: Warning: Downloading updateexec failed: " + exception.Message); return; } ExecuteScript("updateexec"); } /// /// Executes pre-update script file. /// /// True if succesful, otherwise false. private static async ValueTask ExecutePreUpdateScriptAsync() { Logger.Log("Updater: Downloading preupdateexec."); try { string downloadFile = SafePath.CombineFilePath(GamePath, "preupdateexec"); FileStream fileStream = new FileStream(downloadFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (fileStream) { Stream stream = await SharedHttpClient.GetStreamAsync(updateMirrors[currentUpdateMirrorIndex].URL + "preupdateexec").ConfigureAwait(false); using (stream) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } } catch (Exception exception) { Logger.Log("Updater: Warning: Downloading preupdateexec failed: " + exception.Message); return false; } ExecuteScript("preupdateexec"); return true; } /// /// Executes a script file. /// /// Filename of the script file. private static void ExecuteScript(string fileName) { Logger.Log("Updater: Executing " + fileName + "."); FileInfo scriptFileInfo = SafePath.GetFile(GamePath, fileName); var script = new IniFile(scriptFileInfo.FullName); // Delete files. foreach (string key in GetKeys(script, "Delete")) { Logger.Log("Updater: " + fileName + ": Deleting file " + key); try { FileInfo fileInfo = SafePath.GetFile(GamePath, key); if (fileInfo.Exists) { fileInfo.IsReadOnly = false; fileInfo.Delete(); } } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Deleting file " + key + "failed: " + ex.Message); } } // Rename files. foreach (string key in GetKeys(script, "Rename")) { string newFilename = SafePath.CombineFilePath(script.GetStringValue("Rename", key, string.Empty)); if (string.IsNullOrWhiteSpace(newFilename)) continue; try { Logger.Log("Updater: " + fileName + ": Renaming file '" + key + "' to '" + newFilename + "'"); FileInfo srcFile = SafePath.GetFile(GamePath, key); if (srcFile.Exists) { bool isSrcReadOnly = srcFile.IsReadOnly; srcFile.IsReadOnly = false; { FileInfo destFile = SafePath.GetFile(GamePath, newFilename); if (destFile.Exists) { destFile.IsReadOnly = false; destFile.Delete(); } } srcFile.MoveTo(SafePath.CombineFilePath(GamePath, newFilename)); if (isSrcReadOnly) { FileInfo destFile = SafePath.GetFile(GamePath, newFilename); destFile.IsReadOnly = true; } } } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Renaming file '" + key + "' to '" + newFilename + "' failed: " + ex.Message); } } // Rename folders. foreach (string key in GetKeys(script, "RenameFolder")) { string newDirectoryName = script.GetStringValue("RenameFolder", key, string.Empty); if (string.IsNullOrWhiteSpace(newDirectoryName)) continue; try { Logger.Log("Updater: " + fileName + ": Renaming directory '" + key + "' to '" + newDirectoryName + "'"); DirectoryInfo srcDirectory = SafePath.GetDirectory(GamePath, key); if (srcDirectory.Exists) srcDirectory.MoveTo(SafePath.CombineDirectoryPath(GamePath, newDirectoryName)); } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Renaming directory '" + key + "' to '" + newDirectoryName + "' failed: " + ex.Message); } } // Rename & merge files / folders. foreach (string key in GetKeys(script, "RenameAndMerge")) { string directoryName = key; string directoryNameToMergeInto = script.GetStringValue("RenameAndMerge", key, string.Empty); if (string.IsNullOrWhiteSpace(directoryNameToMergeInto)) continue; try { Logger.Log("Updater: " + fileName + ": Merging directory '" + directoryName + "' with '" + directoryNameToMergeInto + "'"); DirectoryInfo directoryToMergeInto = SafePath.GetDirectory(GamePath, directoryNameToMergeInto); DirectoryInfo gameDirectory = SafePath.GetDirectory(GamePath, directoryName); if (!gameDirectory.Exists) continue; if (!directoryToMergeInto.Exists) { Logger.Log("Updater: " + fileName + ": Destination directory '" + directoryNameToMergeInto + "' does not exist, renaming."); gameDirectory.MoveTo(directoryToMergeInto.FullName); } else { Logger.Log("Updater: " + fileName + ": Destination directory '" + directoryNameToMergeInto + "' exists, performing selective merging."); FileInfo[] files = gameDirectory.GetFiles(); foreach (FileInfo file in files) { bool isSrcReadOnly = file.IsReadOnly; file.IsReadOnly = false; FileInfo fileToMergeInto = SafePath.GetFile(directoryToMergeInto.FullName, file.Name); if (fileToMergeInto.Exists) { Logger.Log("Updater: " + fileName + ": Destination file '" + directoryNameToMergeInto + "/" + file.Name + "' exists, removing original source file " + directoryName + "/" + file.Name); // Note: Previously, the incorrect file was deleted as of commit fc939a06ff978b51daa6563eaa15a28cf48319ec. // Remove the original source file file.Delete(); } else { Logger.Log("Updater: " + fileName + ": Destination file '" + directoryNameToMergeInto + "/" + file.Name + "' does not exist, moving original source file " + directoryName + "/" + file.Name); file.MoveTo(fileToMergeInto.FullName); // Resume the read-only property fileToMergeInto.Refresh(); fileToMergeInto.IsReadOnly = isSrcReadOnly; } } } } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Merging directory '" + directoryName + "' with '" + directoryNameToMergeInto + "' failed: " + ex.Message); } } // Delete folders. foreach (string sectionName in new string[] { "DeleteFolder", "ForceDeleteFolder" }) { foreach (string key in GetKeys(script, sectionName)) { try { Logger.Log("Updater: " + fileName + ": Deleting directory '" + key + "'"); DirectoryInfo directoryInfo = SafePath.GetDirectory(GamePath, key); if (directoryInfo.Exists) { // Unset read-only attribute from all files in the directory. foreach (FileInfo file in directoryInfo.GetFiles("*", SearchOption.AllDirectories)) { file.IsReadOnly = false; } directoryInfo.Delete(true); } } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Deleting directory '" + key + "' failed: " + ex.Message); } } } // Delete folders, if empty. foreach (string key in GetKeys(script, "DeleteFolderIfEmpty")) { try { Logger.Log("Updater: " + fileName + ": Deleting directory '" + key + "' if it's empty."); DirectoryInfo directoryInfo = SafePath.GetDirectory(GamePath, key); if (directoryInfo.Exists) { if (!directoryInfo.EnumerateFiles().Any()) { directoryInfo.Delete(); } else { Logger.Log("Updater: " + fileName + ": Directory '" + key + "' is not empty!"); } } else { Logger.Log("Updater: " + fileName + ": Specified directory does not exist."); } } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Deleting directory '" + key + "' if it's empty failed: " + ex.Message); } } // Create folders. foreach (string key in GetKeys(script, "CreateFolder")) { try { DirectoryInfo directoryInfo = SafePath.GetDirectory(GamePath, key); if (!directoryInfo.Exists) { Logger.Log("Updater: " + fileName + ": Creating directory '" + key + "'"); directoryInfo.Create(); } else { Logger.Log("Updater: " + fileName + ": Directory '" + key + "' already exists."); } } catch (Exception ex) { Logger.Log("Updater: " + fileName + ": Creating directory '" + key + "' failed: " + ex.Message); } } scriptFileInfo.Delete(); } /// /// Handle version check. /// private static void VersionCheckHandle() { Logger.Log("Updater: Gathering list of files to be downloaded. Server file info count: " + ServerFileInfos.Count); FileInfosToDownload.Clear(); for (int i = 0; i < ServerFileInfos.Count; i++) { string identifier = ServerFileInfos[i].Identifier; bool flag = false; FileInfo serverFileInfo = SafePath.GetFile(GamePath, ServerFileInfos[i].Filename); for (int k = 0; k < LocalFileInfos.Count; k++) { UpdaterFileInfo info = LocalFileInfos[k]; if (ServerFileInfos[i].Filename == info.Filename) { flag = true; if (!serverFileInfo.Exists) { Logger.Log("Updater: File " + ServerFileInfos[i].Filename + " not found. Adding it to the download queue."); FileInfosToDownload.Add(ServerFileInfos[i]); } else if (info.Identifier != identifier) { Logger.Log("Updater: Local file " + info.Filename + " is different, adding it to the download queue."); FileInfosToDownload.Add(ServerFileInfos[i]); } } } if (!flag) { Logger.Log("Updater: File " + ServerFileInfos[i].Filename + " doesn't exist on local version information - checking if it exists in the directory."); if (serverFileInfo.Exists) { if (TryGetUniqueId(ServerFileInfos[i].Filename) != identifier) { Logger.Log("Updater: File " + ServerFileInfos[i].Filename + " is out of date. Adding it to the download queue."); FileInfosToDownload.Add(ServerFileInfos[i]); } else { Logger.Log("Updater: File " + ServerFileInfos[i].Filename + " exists in the directory and is up to date."); } } else { Logger.Log("Updater: File " + ServerFileInfos[i].Filename + " not found. Adding it to the download queue."); FileInfosToDownload.Add(ServerFileInfos[i]); } } } UpdateSizeInKb = 0; for (int j = 0; j < FileInfosToDownload.Count; j++) { UpdateSizeInKb += FileInfosToDownload[j].Archived ? FileInfosToDownload[j].ArchiveSize : FileInfosToDownload[j].Size; } VersionState = VersionState.OUTDATED; ManualUpdateRequired = false; DoFileIdentifiersUpdatedEvent(); } /// /// Verifies local file version info. /// private static void VerifyLocalFileVersions() { Logger.Log("Verifying local file versions. Count: " + LocalFileInfos.Count); for (int i = 0; i < LocalFileInfos.Count; i++) { UpdaterFileInfo info = LocalFileInfos[i]; if (!ContainsAnyMask(info.Filename)) { if (SafePath.GetFile(GamePath, info.Filename).Exists) { string uniqueIdForFile = GetUniqueIdForFile(info.Filename); if (uniqueIdForFile != info.Identifier) { Logger.Log("Invalid unique identifier for " + info.Filename + "!"); info.Identifier = uniqueIdForFile; } } else { Logger.Log("File " + info.Filename + " does not exist!"); LocalFileInfos.RemoveAt(i); i--; } if (LocalFileInfos.Count > 0) LocalFileCheckProgressChanged?.Invoke(i + 1, LocalFileInfos.Count); } } } /// /// Downloads files required for update and starts second-stage updater. /// private static async Task PerformUpdateAsync() { Logger.Log("Updater: Starting update."); VersionState = VersionState.UPDATEINPROGRESS; try { UpdateUserAgent(SharedHttpClient); SharedProgressMessageHandler.HttpReceiveProgress += ProgressMessageHandlerOnHttpReceiveProgress; if (!await ExecutePreUpdateScriptAsync().ConfigureAwait(false)) throw new("Executing preupdateexec failed."); VerifyLocalFileVersions(); VersionCheckHandle(); if (string.IsNullOrEmpty(ServerGameVersion) || ServerGameVersion == "N/A" || VersionState != VersionState.OUTDATED) throw new("Update server integrity error."); VersionState = VersionState.UPDATEINPROGRESS; totalDownloadedKbs = 0; if (terminateUpdate) { Logger.Log("Updater: Terminating update because of user request."); VersionState = VersionState.OUTDATED; ManualUpdateRequired = false; terminateUpdate = false; } else { foreach (UpdaterFileInfo info in FileInfosToDownload) { int num = 0; if (terminateUpdate) { Logger.Log("Updater: Terminating update because of user request."); VersionState = VersionState.OUTDATED; ManualUpdateRequired = false; terminateUpdate = false; return; } while (true) { currentFilename = info.Archived ? info.Filename + ARCHIVE_FILE_EXTENSION : info.Filename; currentFileSize = info.Archived ? info.ArchiveSize : info.Size; string errorMessage = await DownloadFileAsync(info).ConfigureAwait(false); if (terminateUpdate) { Logger.Log("Updater: Terminating update because of user request."); VersionState = VersionState.OUTDATED; ManualUpdateRequired = false; terminateUpdate = false; return; } if (errorMessage is null) { totalDownloadedKbs += info.Archived ? info.ArchiveSize : info.Size; break; } num++; if (num == 2) { Logger.Log("Updater: Too many retries for downloading file " + (info.Archived ? info.Filename + ARCHIVE_FILE_EXTENSION : info.Filename) + ". Update halted."); string extraMsg = Environment.NewLine + Environment.NewLine + "Download error message: " + errorMessage; throw new("Too many retries for downloading file " + (info.Archived ? info.Filename + ARCHIVE_FILE_EXTENSION : info.Filename) + extraMsg); } } } if (terminateUpdate) { Logger.Log("Updater: Terminating update because of user request."); VersionState = VersionState.OUTDATED; ManualUpdateRequired = false; terminateUpdate = false; } else { Logger.Log("Updater: Downloading files finished - copying from temporary updater directory."); await ExecuteAfterUpdateScriptAsync().ConfigureAwait(false); Logger.Log("Updater: Cleaning up."); // this folder contains incoming files that needs to be updated by second stage updater DirectoryInfo incomingDirectoryInfo = SafePath.GetDirectory(GamePath, "Updater"); FileInfo versionFile = SafePath.GetFile(GamePath, VERSION_FILE); FileInfo versionFileTemp = SafePath.GetFile(GamePath, FormattableString.Invariant($"{VERSION_FILE}_u")); if (incomingDirectoryInfo.Exists) { versionFileTemp.MoveTo(SafePath.CombineFilePath(incomingDirectoryInfo.FullName, VERSION_FILE)); // make sure the existing version file do not exist, to make the legacy "clientupdt.exe" second stage updater happy SafePath.DeleteFileIfExists(versionFile.FullName); } else { // since second stage updater will not be launched, just override the existing version file SafePath.DeleteFileIfExists(versionFile.FullName); versionFileTemp.MoveTo(versionFile.FullName); } FileInfo themeFileInfo = SafePath.GetFile(GamePath, "Theme_c.ini"); if (themeFileInfo.Exists) { Logger.Log("Updater: Theme_c.ini exists -- copying it."); themeFileInfo.CopyTo(SafePath.CombineFilePath(GamePath, "INI", "Theme.ini"), true); Logger.Log("Updater: Theme.ini copied successfully."); } incomingDirectoryInfo.Refresh(); if (incomingDirectoryInfo.Exists) { // update legacy second stage updater DirectoryInfo currentLegacySecondStageUpdaterDirectory = SafePath.GetDirectory(GamePath); FileInfo currentLegacySecondStageUpdaterExecutable = SafePath.GetFile(currentLegacySecondStageUpdaterDirectory.FullName, LEGACY_SECOND_STAGE_UPDATER); DirectoryInfo incomingLegacySecondStageUpdaterDirectory = SafePath.GetDirectory(incomingDirectoryInfo.FullName); FileInfo incomingLegacySecondStageUpdaterExecutable = SafePath.GetFile(incomingLegacySecondStageUpdaterDirectory.FullName, LEGACY_SECOND_STAGE_UPDATER); if (incomingLegacySecondStageUpdaterExecutable.Exists) { SafePath.DeleteFileIfExists(currentLegacySecondStageUpdaterExecutable.FullName); incomingLegacySecondStageUpdaterExecutable.MoveTo(currentLegacySecondStageUpdaterExecutable.FullName); currentLegacySecondStageUpdaterExecutable.Refresh(); } #region update-second-stage-updater // the second stage updater is placed at "Resources\Binaries\Updater" directory. DirectoryInfo currentSecondStageUpdaterDirectory = SafePath.GetDirectory(ResourcePath, BINARIES_FOLDER, "Updater"); if (!currentSecondStageUpdaterDirectory.Exists) currentSecondStageUpdaterDirectory.Create(); FileInfo secondStageUpdaterExecutable = SafePath.GetFile(currentSecondStageUpdaterDirectory.FullName, SECOND_STAGE_UPDATER); // update the new second stage updater before other files DirectoryInfo incomingSecondStageUpdaterDirectory = SafePath.GetDirectory(incomingDirectoryInfo.FullName, "Resources", BINARIES_FOLDER, "Updater"); if (incomingSecondStageUpdaterDirectory.Exists) { Logger.Log("Updater: Checking & moving second-stage updater files."); // copy SecondStageUpdater IEnumerable updaterFiles = incomingSecondStageUpdaterDirectory.EnumerateFiles(Path.GetFileNameWithoutExtension(SECOND_STAGE_UPDATER) + ".*"); foreach (FileInfo updaterFile in updaterFiles) { FileInfo updaterFileResource = SafePath.GetFile(currentSecondStageUpdaterDirectory.FullName, updaterFile.Name); Logger.Log("Updater: Moving second-stage updater file " + updaterFile.Name + "."); SafePath.DeleteFileIfExists(updaterFileResource.FullName); updaterFile.MoveTo(updaterFileResource.FullName); } // copy SecondStageUpdater dependencies // warning: for unknown reasons, `System.Runtime.CompilerServices.Unsafe.dll` file is not listed here. // Therefore, Polyfill (requiring this dll file) is excluded from the second-stage updater. AssemblyName[] assemblies = Assembly.LoadFrom(secondStageUpdaterExecutable.FullName).GetReferencedAssemblies(); foreach (AssemblyName assembly in assemblies) { FileInfo incomingAssemblyFile = SafePath.GetFile(incomingSecondStageUpdaterDirectory.FullName, FormattableString.Invariant($"{assembly.Name}.dll")); if (!incomingAssemblyFile.Exists) { Logger.Log("Updater: Missing assembly file required by second-stage updater: " + incomingAssemblyFile.Name + "."); continue; } FileInfo currentAssemblyFile = SafePath.GetFile(currentSecondStageUpdaterDirectory.FullName, incomingAssemblyFile.Name); Logger.Log("Updater: Moving second-stage updater file " + incomingAssemblyFile.Name + "."); SafePath.DeleteFileIfExists(currentAssemblyFile.FullName); incomingAssemblyFile.MoveTo(currentAssemblyFile.FullName); } } #endregion Logger.Log("Updater: Launching second-stage updater executable " + secondStageUpdaterExecutable.FullName + "."); // fallback to the old "clientupdt.dat" file if the new second-stage updater does not exist bool runNativeWindowsExe = true; #if !NETFRAMEWORK runNativeWindowsExe = false; #endif if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !secondStageUpdaterExecutable.Exists) { Logger.Log("Updater: Missing second-stage updater executable " + secondStageUpdaterExecutable.FullName + "."); if (currentLegacySecondStageUpdaterExecutable.Exists) { Logger.Log("Updater: Falling back to legacy second-stage updater executable " + currentLegacySecondStageUpdaterExecutable.FullName + "."); secondStageUpdaterExecutable = currentLegacySecondStageUpdaterExecutable; runNativeWindowsExe = true; } } ProcessStartInfo secondStageUpdaterStartInfo; if (runNativeWindowsExe) { // e.g. C:\Game\Resources\SecondStageUpdater.exe clientogl.exe "C:\Game\" secondStageUpdaterStartInfo = new ProcessStartInfo { FileName = secondStageUpdaterExecutable.FullName, Arguments = CallingExecutableFileName + " \"" + GamePath + "\"", UseShellExecute = false, }; } else { // e.g. dotnet "C:\Game\Resources\SecondStageUpdater.dll" clientogl.dll "C:\Game\" secondStageUpdaterStartInfo = new ProcessStartInfo { FileName = "dotnet", Arguments = "\"" + secondStageUpdaterExecutable.FullName + "\" " + CallingExecutableFileName + " \"" + GamePath + "\"", UseShellExecute = true, }; } Logger.Log("Updater: Launching second-stage updater executable."); Logger.Log("Updater: FileName = " + secondStageUpdaterStartInfo.FileName); Logger.Log("Updater: Arguments = " + secondStageUpdaterStartInfo.Arguments); Logger.Log("Updater: UseShellExecute = " + secondStageUpdaterStartInfo.UseShellExecute); using var _ = Process.Start(secondStageUpdaterStartInfo); Restart?.Invoke(null, EventArgs.Empty); } else { Logger.Log("Updater: Update completed successfully."); totalDownloadedKbs = 0; UpdateSizeInKb = 0; CheckLocalFileVersions(); ServerGameVersion = "N/A"; VersionState = VersionState.UPTODATE; DoUpdateCompleted(); if (AreCustomComponentsOutdated()) DoCustomComponentsOutdatedEvent(); } } } } catch (Exception exception) { Logger.Log("Updater: An error occurred during the update. Message: " + exception.Message); VersionState = VersionState.UNKNOWN; DoOnUpdateFailed(exception); } finally { SharedProgressMessageHandler.HttpReceiveProgress -= ProgressMessageHandlerOnHttpReceiveProgress; } } /// /// Downloads and handles individual file. /// /// File info for the file. /// Error message if something went wrong, otherwise null. private static async ValueTask DownloadFileAsync(UpdaterFileInfo fileInfo) { Logger.Log("Updater: Initializing download of file " + fileInfo.Filename); UpdateDownloadProgress(0); string filename = fileInfo.Filename; const string prefixPath = "Updater"; FileInfo decompressedFile = SafePath.GetFile(GamePath, prefixPath, filename); try { string uriString = string.Empty; int currentUpdateMirrorId = Updater.currentUpdateMirrorIndex; string extraExtension = fileInfo.Archived ? ARCHIVE_FILE_EXTENSION : string.Empty; string fileRelativePath = SafePath.CombineFilePath(prefixPath, FormattableString.Invariant($"{filename}{extraExtension}")); uriString = (updateMirrors[currentUpdateMirrorId].URL + filename + extraExtension).Replace('\\', '/'); FileInfo downloadFile = SafePath.GetFile(GamePath, fileRelativePath); CreatePath(SafePath.CombineFilePath(GamePath, filename)); CreatePath(downloadFile.FullName); if (downloadFile.Exists && (fileInfo.Archived ? fileInfo.Identifier : fileInfo.ArchiveIdentifier) == GetUniqueIdForFile(fileRelativePath)) { Logger.Log("Updater: File " + filename + " has already been downloaded, skipping downloading."); } else { Logger.Log("Updater: Downloading file " + filename + extraExtension); FileStream fileStream = new FileStream(downloadFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); using (fileStream) { Stream stream = await SharedHttpClient.GetStreamAsync(new Uri(uriString)).ConfigureAwait(false); using (stream) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } OnFileDownloadCompleted?.Invoke(fileInfo.Archived ? filename + extraExtension : null); Logger.Log("Updater: Download of file " + filename + extraExtension + " finished - verifying."); if (fileInfo.Archived) { Logger.Log("Updater: File is an archive."); string archiveIdentifier = CheckFileIdentifiers(filename, fileRelativePath, fileInfo.ArchiveIdentifier); if (string.IsNullOrEmpty(archiveIdentifier)) { Logger.Log("Updater: Archive " + filename + extraExtension + " is intact. Unpacking..."); await CompressionHelper.DecompressFileAsync(downloadFile.FullName, decompressedFile.FullName).ConfigureAwait(false); downloadFile.Delete(); } else { string errorMsg = "Downloaded archive " + filename + extraExtension + " has a non-matching identifier: " + archiveIdentifier + " against " + fileInfo.ArchiveIdentifier; Logger.Log("Updater: " + errorMsg); DeleteFileAndWait(downloadFile.FullName); return errorMsg; } } #if !NETFRAMEWORK if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && downloadFile.Extension.Equals(".sh", StringComparison.OrdinalIgnoreCase)) { Logger.Log($"Updater: File {downloadFile.Name} is a script, adding execute permission. Current permission flags: " + downloadFile.UnixFileMode); downloadFile.Refresh(); downloadFile.UnixFileMode |= UnixFileMode.UserExecute; downloadFile.Refresh(); Logger.Log($"Updater: File {downloadFile.Name} execute permission added. Current permission flags: " + downloadFile.UnixFileMode); } #endif } string fileIdentifier = CheckFileIdentifiers(filename, SafePath.CombineFilePath(prefixPath, filename), fileInfo.Identifier); if (string.IsNullOrEmpty(fileIdentifier)) { Logger.Log("Updater: File " + filename + " is intact."); return null; } string msg = "Downloaded file " + filename + " has a non-matching identifier: " + fileIdentifier + " against " + fileInfo.Identifier; Logger.Log("Updater: " + msg); DeleteFileAndWait(decompressedFile.FullName); return msg; } catch (Exception exception) { Logger.Log("Updater: An error occurred while downloading file " + filename + ": " + exception.Message); DeleteFileAndWait(decompressedFile.FullName); return exception.Message; } } /// /// Updates download progress. /// /// Progress percentage. private static void UpdateDownloadProgress(int progressPercentage) { double num = currentFileSize * (progressPercentage / 100.0); double num2 = totalDownloadedKbs + num; int totalPercentage = 0; if (UpdateSizeInKb is > 0 and < int.MaxValue) totalPercentage = (int)(num2 / UpdateSizeInKb * 100.0); DownloadProgressChanged(currentFilename, progressPercentage, totalPercentage); } /// /// Checks if file path contains ignore masks. /// /// File path to check. /// True if path contains any ignore masks, otherwise false. private static bool ContainsAnyMask(string filePath) { foreach (string mask in ignoreMasks) { if (filePath.Contains(mask, StringComparison.OrdinalIgnoreCase)) return true; } return false; } /// /// Gets keys from INI file section. /// /// INI file. /// Section name. /// List of keys or empty list if section does not exist or no keys were found. private static List GetKeys(IniFile iniFile, string sectionName) { List keys = iniFile.GetSectionKeys(sectionName); if (keys != null) return keys; return new(); } /// /// Attempts to get file identifier for a file. /// /// File path of file. /// File identifier if successful, otherwise empty string. private static string TryGetUniqueId(string filePath) { try { return GetUniqueIdForFile(filePath); } catch { return string.Empty; } } /// /// Checks file identifiers to see if file is intact. /// /// Filename in file info. /// Filename on system. /// Current file identifier. /// File identifier if check is successful, otherwise null. private static string CheckFileIdentifiers(string fileInfoFilename, string localFilename, string fileInfoIdentifier) { if (ContainsAnyMask(fileInfoFilename)) return null; string identifier; identifier = GetUniqueIdForFile(localFilename); return fileInfoIdentifier == identifier ? null : identifier; } public static event NoParamEventHandler FileIdentifiersUpdated; public static event LocalFileCheckProgressChangedCallback LocalFileCheckProgressChanged; public static event NoParamEventHandler OnCustomComponentsOutdated; public static event NoParamEventHandler OnLocalFileVersionsChecked; public static event NoParamEventHandler OnUpdateCompleted; public static event SetExceptionCallback OnUpdateFailed; public static event NoParamEventHandler OnVersionStateChanged; public static event FileDownloadCompletedEventHandler OnFileDownloadCompleted; public static event EventHandler Restart; public static event UpdateProgressChangedCallback UpdateProgressChanged; public delegate void LocalFileCheckProgressChangedCallback(int checkedFileCount, int totalFileCount); public delegate void NoParamEventHandler(); public delegate void SetExceptionCallback(Exception ex); public delegate void UpdateProgressChangedCallback(string currFileName, int currFilePercentage, int totalPercentage); public delegate void FileDownloadCompletedEventHandler(string archiveName); private static void ProgressMessageHandlerOnHttpReceiveProgress(object sender, HttpProgressEventArgs e) => UpdateDownloadProgress(e.ProgressPercentage); private static void DownloadProgressChanged(string currFileName, int currentFilePercentage, int totalPercentage) => UpdateProgressChanged?.Invoke(currFileName, currentFilePercentage, totalPercentage); private static void DoCustomComponentsOutdatedEvent() => OnCustomComponentsOutdated?.Invoke(); private static void DoFileIdentifiersUpdatedEvent() { Logger.Log("Updater: File identifiers updated."); FileIdentifiersUpdated?.Invoke(); } private static void DoOnUpdateFailed(Exception ex) => OnUpdateFailed?.Invoke(ex); private static void DoOnVersionStateChanged() => OnVersionStateChanged?.Invoke(); private static void DoUpdateCompleted() => OnUpdateCompleted?.Invoke(); } ================================================ FILE: ClientUpdater/UpdaterFileInfo.cs ================================================ // Copyright 2022-2024 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . namespace ClientUpdater; /// /// Updater file info. /// internal sealed record UpdaterFileInfo(string Filename, int Size) { public string Identifier { get; set; } public string ArchiveIdentifier { get; set; } public int ArchiveSize { get; set; } public bool Archived => !string.IsNullOrEmpty(ArchiveIdentifier) && ArchiveSize > 0; } ================================================ FILE: ClientUpdater/VersionState.cs ================================================ // Copyright 2022-2024 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . namespace ClientUpdater; /// /// Updater version state. /// public enum VersionState { UPTODATE, MISMATCHED, UNKNOWN, UPDATEINPROGRESS, UPDATECHECKINPROGRESS, OUTDATED } ================================================ FILE: CommonAssemblies.txt ================================================ ClientUpdater.dll ClientUpdater.pdb Cyotek.Drawing.BitmapFont.dll DiscordRPC.dll Facepunch.Steamworks.Win64.dll FontStashSharp.Base.dll FontStashSharp.Rasterizers.StbTrueTypeSharp.dll FontStashSharp.TextShapers.HarfBuzz.dll HarfBuzzSharp.dll lzo.net.dll Microsoft.Extensions.Configuration.Abstractions.dll Microsoft.Extensions.Configuration.Binder.dll Microsoft.Extensions.Configuration.CommandLine.dll Microsoft.Extensions.Configuration.dll Microsoft.Extensions.Configuration.EnvironmentVariables.dll Microsoft.Extensions.Configuration.FileExtensions.dll Microsoft.Extensions.Configuration.Json.dll Microsoft.Extensions.Configuration.UserSecrets.dll Microsoft.Extensions.DependencyInjection.Abstractions.dll Microsoft.Extensions.DependencyInjection.dll Microsoft.Extensions.Diagnostics.Abstractions.dll Microsoft.Extensions.Diagnostics.dll Microsoft.Extensions.FileProviders.Abstractions.dll Microsoft.Extensions.FileProviders.Physical.dll Microsoft.Extensions.FileSystemGlobbing.dll Microsoft.Extensions.Hosting.Abstractions.dll Microsoft.Extensions.Hosting.dll Microsoft.Extensions.Logging.Abstractions.dll Microsoft.Extensions.Logging.Configuration.dll Microsoft.Extensions.Logging.Console.dll Microsoft.Extensions.Logging.Debug.dll Microsoft.Extensions.Logging.dll Microsoft.Extensions.Logging.EventLog.dll Microsoft.Extensions.Logging.EventSource.dll Microsoft.Extensions.Options.ConfigurationExtensions.dll Microsoft.Extensions.Options.dll Microsoft.Extensions.Primitives.dll Newtonsoft.Json.Bson.dll Newtonsoft.Json.dll OpenMcdf.dll Rampastring.Tools.dll Rampastring.Tools.pdb Rampastring.Tools.xml SixLabors.ImageSharp.dll System.CodeDom.dll StbImageSharp.dll StbTrueTypeSharp.dll steam_api64.dll System.Net.Http.Formatting.dll TextCopy.dll ================================================ FILE: CommonAssembliesNetFx.txt ================================================ ClientUpdater.dll ClientUpdater.pdb Cyotek.Drawing.BitmapFont.dll DiscordRPC.dll Facepunch.Steamworks.Win64.dll FontStashSharp.Base.dll FontStashSharp.Rasterizers.StbTrueTypeSharp.dll FontStashSharp.TextShapers.HarfBuzz.dll HarfBuzzSharp.dll libHarfBuzzSharp.dylib lzo.net.dll Microsoft.Bcl.AsyncInterfaces.dll Microsoft.Extensions.Configuration.Abstractions.dll Microsoft.Extensions.Configuration.Binder.dll Microsoft.Extensions.Configuration.CommandLine.dll Microsoft.Extensions.Configuration.dll Microsoft.Extensions.Configuration.EnvironmentVariables.dll Microsoft.Extensions.Configuration.FileExtensions.dll Microsoft.Extensions.Configuration.Json.dll Microsoft.Extensions.Configuration.UserSecrets.dll Microsoft.Extensions.DependencyInjection.Abstractions.dll Microsoft.Extensions.DependencyInjection.dll Microsoft.Extensions.Diagnostics.Abstractions.dll Microsoft.Extensions.Diagnostics.dll Microsoft.Extensions.FileProviders.Abstractions.dll Microsoft.Extensions.FileProviders.Physical.dll Microsoft.Extensions.FileSystemGlobbing.dll Microsoft.Extensions.Hosting.Abstractions.dll Microsoft.Extensions.Hosting.dll Microsoft.Extensions.Logging.Abstractions.dll Microsoft.Extensions.Logging.Configuration.dll Microsoft.Extensions.Logging.Console.dll Microsoft.Extensions.Logging.Debug.dll Microsoft.Extensions.Logging.dll Microsoft.Extensions.Logging.EventLog.dll Microsoft.Extensions.Logging.EventSource.dll Microsoft.Extensions.Options.ConfigurationExtensions.dll Microsoft.Extensions.Options.dll Microsoft.Extensions.Primitives.dll Newtonsoft.Json.Bson.dll Newtonsoft.Json.dll OpenMcdf.dll Rampastring.Tools.dll Rampastring.Tools.pdb Rampastring.Tools.xml SixLabors.ImageSharp.dll StbImageSharp.dll StbTrueTypeSharp.dll steam_api64.dll System.Buffers.dll System.CodeDom.dll System.Diagnostics.DiagnosticSource.dll System.IO.FileSystem.AccessControl.dll System.Memory.dll System.Net.Http.Formatting.dll System.Numerics.Vectors.dll System.Runtime.CompilerServices.Unsafe.dll System.Security.AccessControl.dll System.Security.Principal.Windows.dll System.Text.Encoding.CodePages.dll System.Text.Encodings.Web.dll System.Text.Json.dll System.Threading.Tasks.Extensions.dll System.ValueTuple.dll TextCopy.dll ================================================ FILE: Contributing.md ================================================ # Contributing This file lists the contributing guidelines that are used in the project. ### Commit style guide Commits start with a capital letter and don't end in a punctuation mark. Right: ``` Treat usernames as case-insensitive in user collections ``` Wrong: ``` treat usernames as case-insensitive in user collections. ``` Use imperative present tense in commit messages instead of past tense. Right: ``` Add null-check for GameMode ``` Wrong: ``` Added null-check for GameMode ``` ### Pull Requests Make sure that the scope of your pull request is well defined. Pull requests can take significant developer time to review and very large pull requests or pull requests with poorly defined scope can be difficult to review. One pull request should _only implement one feature_ or _fix one bug_, unless there is a good reason for grouping the changes together. Do not heavily refactor the style of existing code in a pull request, unless the refactored code fits to the scope of the pull request (feature or bug fix). Rather, if you want to refactor existing code just for the sake of refactoring or getting rid of technical debt, create a secondary pull request for that purpose. If you have introduced a new DLL dependency, check [README for Build Scripts](./Scripts/README.md) to determine whether you need to update the common assembly list and how to do that. **Make sure your code and commits match this style guide before you create your pull request.** Pull requests that are not well defined in their scope or pull requests that don't match the style guide can end up rejected and closed by the staff. ### Code style guide We have established a couple of code style rules to keep things consistent. Please check your code style before committing the code. - We use spaces instead of tabs to indent code. - Curly braces are always to be placed on a new line. One of the reasons for this is to clearly separate the end of the code block head and body in case of multiline bodies: ```cs if (SomeReallyLongCondition() || ThatSplitsIntoMultipleLines()) { DoSomethingHere(); DoSomethingMore(); } ``` - Braceless code block bodies should be made only when both code block head and body are single line. Statements that split into multiple lines and nested braceless blocks are not allowed within braceless blocks: ```cs // OK if (Something()) DoSomething(); // OK if (SomeReallyLongCondition() || ThatSplitsIntoMultipleLines()) { DoSomething(); } // OK if (SomeCondition()) { if (SomeOtherCondition()) DoSomething(); } // OK if (SomeCondition()) { return VeryLongExpression() || ThatSplitsIntoMultipleLines(); } ``` - Only empty curly brace blocks may be left on the same line for both opening and closing braces (if appropriate). - If you use `if`-`else` you should either have all of the code blocks braced or braceless to keep things consistent. - Code should have empty lines to make it easier to read. Use an empty line to split code into logical parts. It's mandatory to have empty lines to separate: - `return` statements (except when there is only one line of code except that statement); - local variable assignments that are used in the further code (you shouldn't put an empty line after one-line local variable assignments that are used only in the following code block though); - code blocks (braceless or not) or anything using code blocks (function or hook definitions, classes, namespaces etc.) ```cs // OK int localVar = Something(); if (SomeConditionUsing(localVar)) ... // OK int localVar = Something(); int anotherLocalVar = OtherSomething(); if (SomeConditionUsing(localVar, anotherLocalVar)) ... // OK int localVar = Something(); if (SomeConditionUsing(localVar)) ... if (SomeOtherConditionUsing(localVar)) ... localVar = OtherSomething(); // OK if (SomeCondition()) { Code(); OtherCode(); return; } // OK if (SomeCondition()) { SmallCode(); return; } ``` - Use `var` with local variables when the type of the variable is obvious from the code or the type is not relevant. Never use `var` with primitive types. - A space must be put between braces of empty curly brace blocks. ```cs // OK var list = new List(); // Not OK var something = 6; ``` - Local variables, function/method args and private class fields are named in `camelCase` and a descriptive name, like `ircUser` for a local `IrcUser` variable. - Classes, namespaces, and properties are always written in `PascalCase`. - Class fields that can be set via INI tags should be named exactly like ini tags with dots replaced with underscores. #### Formatter requirements - If you have made medium or significant changes to a file (> 25%), you should run the code formatter on the whole file using Visual Studio. - If you have only made minor changes to a file (≤ 25%), you should only format the lines that you have changed to keep the style of the file consistent. - You should apply the removal and sorting of `using` directives to the whole file if one of the following is true, and you should not apply it otherwise: - You have reached the threshold for running the code formatter on the whole file, or - You have added and/or removed `using` directives, especially if you have added AND removed `using` directives. #### C# nullability requirements The project has mixed usages of nullability annotations. - When you are adding new `.cs` files, you must write `#nullable enable` at the top of the file and make sure that all code in that file is null-safe. - When you are modifying existing `.cs` files, if you have made significant changes to the file (> 75%), you should write `#nullable enable` at the top of the file and make sure that all code in that file is null-safe. If you are only making minor or medium changes to an existing `.cs` file (≤ 75%) that does not start with `#nullable enable`, you should write code without nullability annotations to keep the style of the file consistent. ### Forbidden APIs - You should not use `BitConverter`, because its behavior depends on platform endianness via `BitConverter.IsLittleEndian`. Instead, you should use `BinaryPrimitives` for byte conversions. ### Text encoding - Before converting between byte arrays and strings, you should always think carefully about the encoding to be used. - For client-side text, you should use UTF-8 encoding without BOM, unless you have a good reason not to. - For game-related text, you should carefully examine the encoding used by the game and use that encoding for conversions, and you MUST also check if the encoding is the retrieved encoding or the system ANSI encoding (which varies by system locale), and use the correct one accordingly. Use ASCII encoding if you can't determine the encoding and the string seems to only contain ASCII characters. - Example: if you get a Windows-1252 encoding from the game, it might be either a constant usage of Windows-1252 encoding or the system ANSI encoding, so you should check by making sure the string contains at least one non-ASCII character, running the game in a virtual machine with a different system locale (e.g. Russian, Chinese, Polish) and observing whether the encoding changes. ### Literal strings - This codebase contains a literal string localization system. Use `"literal string".L10N("key")` to mark literal strings for localization. This extension method requires `using ClientCore.Extensions;` to be in scope. - You must make sure both the literal string and the key are compile-time constant and they must be consistent across all platforms. Use `/` for the path separator and `\n` for the line break in the literal string, instead of using `Environment.NewLine` or `Path.DirectorySeparatorChar`. The key must be in the format of `Namespace:SubNamespace:...:KeyName` and should be as descriptive as possible to make it easier for translators to understand the context. Below demonstrates some examples of bad usages violating the constant requirement: - Do not localize non-literal strings. ```cs // OK string greetingText = string.Format("Hello, {0}!".L10N("Client:Main:GreetingMessage"), userName); // Not OK string greetingText = string.Format("Hello, {0}!", userName).L10N("Client:Main:GreetingMessage"); // Not OK string greetingText = $"Hello, {userName}!"; // Not OK string greetingText = $"Hello, {userName}!".L10N("Client:Main:GreetingMessage"); ``` - Do not conditionally determine the key or the literal string. ```cs // OK bool isSuccess = DoSomething(); string message = isSuccess ? "Operation succeeded.".L10N("Client:Main:OperationSucceededMessage") : "Operation failed.".L10N("Client:Main:OperationFailedMessage"); // Not OK bool isSuccess = DoSomething(); string message = (isSuccess ? "Operation succeeded." : "Operation failed.").L10N(isSuccess ? "Client:Main:OperationSucceededMessage" : "Client:Main:OperationFailedMessage"); // OK int resultErrorCode = DoSomething(); string errorMessage = resultErrorCode switch { 0 => "Operation succeeded.".L10N("Client:Main:OperationSucceededMessage"), 1 => "Operation failed.".L10N("Client:Main:OperationFailedMessage"), 2 => "Operation failed due to file not found.".L10N("Client:Main:FileNotFoundMessage"), _ => "Operation failed due to unknown error.".L10N("Client:Main:UnknownErrorMessage") }; // Not OK int resultErrorCode = DoSomething(); string errorMessage = resultErrorCode switch { 0 => "Operation succeeded.", 1 => "Operation failed.", 2 => "Operation failed due to file not found.", _ => "Operation failed due to unknown error." }.L10N($"Client:Main:ResultErrorCode{resultErrorCode}Message"); ``` - Consider the timing when a static class member gets initialized. Use getters `=>` instead of fields `=` for static class members that are initialized with literal strings to make sure the localization system is properly initialized before the literal strings get localized. ```cs // OK class MyClass { public static string GreetingMessage => "Hello, world!".L10N("Client:MyClass:GreetingMessage"); } // Not OK class MyClass { public static string GreetingMessage = "Hello, world!".L10N("Client:MyClass:GreetingMessage"); } ``` - The literal string must not start or end with whitespace. Use `"literal string".L10N("key") + " "` if you need to add whitespace at the end of the literal string for formatting reasons. ```cs // OK string message = "An error occurred. Error:".L10N("Client:Main:ErrorMessage") + " " + errorDetails; // Not OK string message = "An error occurred. Error: ".L10N("Client:Main:ErrorMessage") + errorDetails; // Not OK string message = "An error occurred. Error:".L10N("Client:Main:ErrorMessage") + errorDetails; // This violates the English punctuation rules ``` Note: This guide is not exhaustive and may be adjusted in the future. ================================================ FILE: DXClient.slnx ================================================ ================================================ FILE: DXMainClient/AdminRestarter.cs ================================================ #nullable enable using System; using System.Diagnostics; using System.Runtime.Versioning; using System.Security.Principal; using Rampastring.Tools; using ClientCore; namespace DTAClient { /// /// Utility for restarting the client with administrator privileges. /// [SupportedOSPlatform("windows")] public static class AdminRestarter { /// /// Checks if the application is running with administrator privileges. /// /// True if running as administrator, false otherwise. public static bool IsRunningAsAdministrator() { try { using WindowsIdentity identity = WindowsIdentity.GetCurrent(); WindowsPrincipal principal = new WindowsPrincipal(identity); return principal.IsInRole(WindowsBuiltInRole.Administrator); } catch { return false; } } /// /// Restarts the current application with administrator privileges. /// /// True if the restart was initiated successfully, false otherwise. public static bool RestartAsAdmin() { bool runNativeWindowsExe = true; #if !NETFRAMEWORK runNativeWindowsExe = false; #endif try { if (runNativeWindowsExe) { using var _ = Process.Start(new ProcessStartInfo { FileName = SafePath.CombineFilePath(ProgramConstants.StartupExecutable), Verb = "runas", UseShellExecute = true, }); } else { // Calling dotnet.exe has the following disadvantages: // 1. We need to specify `UseShellExecute = true` for the `Runas` verb, which means we cannot hide the console window despite setting `CreateNoWindow = true`. // 2. For XNA build, we need to call the x86 version of dotnet.exe. // Therefore, we calls the launcher exe with the argument of current platform. This makes the client tightly coupled with the launcher, which is not ideal but acceptable for now. string arguments; #if XNA arguments = "-NET8 -XNA"; #elif DX arguments = "-NET8 -DX"; #elif GL // Note: we can assume no UGL build here because this class is labeled as Windows-only. arguments = "-NET8 -OGL"; #else #error Unknown build configuration #endif using var _ = Process.Start(new ProcessStartInfo { FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.LauncherExe), Verb = "runas", Arguments = arguments, UseShellExecute = true, }); } return true; } catch (Exception ex) { Logger.Log("Failed to restart with admin privileges: " + ex.ToString()); return false; } } } } ================================================ FILE: DXMainClient/DXGUI/Campaign/CampaignCheckBox.cs ================================================ using System; using ClientCore; using DTAClient.DXGUI.Generic; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Campaign; public class CampaignCheckBox : GameSessionCheckBox { public CampaignCheckBox(WindowManager windowManager) : base (windowManager) { } public bool ResetToDefaultOnGameExit { get; private set; } public override void Initialize() { // Find the campaign selector that this control belongs to and register ourselves as a game option. XNAControl parent = Parent; while (true) { if (parent == null) break; // oh no, we have a circular class reference here! if (parent is CampaignSelector configView) { configView.CheckBoxes.Add(this); break; } parent = parent.Parent; } base.Initialize(); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "ResetToDefaultOnGameExit": ResetToDefaultOnGameExit = Conversions.BooleanFromString(value, false); return; case "CustomIniPath" when !ClientConfiguration.Instance.CopyMissionsToSpawnmapINI: throw new Exception($"Campaign settings can't affect map code if {nameof(ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)} is disabled!\n\n" + $"Offending setting control: {Name}"); } base.ParseControlINIAttribute(iniFile, key, value); } } ================================================ FILE: DXMainClient/DXGUI/Campaign/CampaignDropDown.cs ================================================ using System; using ClientCore; using DTAClient.DXGUI.Generic; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Campaign; public class CampaignDropDown : GameSessionDropDown { public CampaignDropDown(WindowManager windowManager) : base (windowManager) { } public override void Initialize() { // Find the campaign selector that this control belongs to and register ourselves as a game option. XNAControl parent = Parent; while (true) { if (parent == null) break; // oh no, we have a circular class reference here! if (parent is CampaignSelector configView) { configView.DropDowns.Add(this); break; } parent = parent.Parent; } base.Initialize(); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "DataWriteMode" && value.ToUpper() == "MAPCODE" && !ClientConfiguration.Instance.CopyMissionsToSpawnmapINI) { throw new Exception($"Campaign settings can't affect map code if {nameof(ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)} is disabled!\n\n" + $"Offending setting control: {Name}"); } base.ParseControlINIAttribute(iniFile, key, value); } } ================================================ FILE: DXMainClient/DXGUI/Campaign/CampaignSelector.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using ClientCore; using ClientCore.Enums; using ClientCore.Extensions; using ClientGUI; using ClientGUI.Settings; using ClientUpdater; using DTAClient.Domain; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Campaign { public class CampaignSelector : XNAWindow { private const int DEFAULT_WIDTH = 650; private const int DEFAULT_HEIGHT = 600; private const string SETTINGS_PATH = "Client/CampaignSettings.ini"; private static string[] DifficultyNames = new string[] { "Easy", "Medium", "Hard" }; private static string[] DifficultyIniPaths = new string[] { "INI/Map Code/Difficulty Easy.ini", "INI/Map Code/Difficulty Medium.ini", "INI/Map Code/Difficulty Hard.ini" }; public CampaignSelector(WindowManager windowManager, DiscordHandler discordHandler, CampaignTagSelector campaignTagSelector) : base(windowManager) { this.discordHandler = discordHandler; this.campaignTagSelector = campaignTagSelector; } private DiscordHandler discordHandler; private CampaignTagSelector campaignTagSelector; private List selectedMissions = []; private XNAPanel pnlMissionPreview; private bool pnlMissionPreviewBackgroundTextureNeedsDispose = false; private string missionPreviewFolder => SafePath.CombineDirectoryPath(ProgramConstants.GetBaseResourcePath(), "Mission Previews"); private string defaultMissionPreviewPath => SafePath.CombineFilePath(missionPreviewFolder, "Default.png"); private bool pnlMissionPreviewEnabled => File.Exists(defaultMissionPreviewPath); private XNAListBox lbCampaignList; private XNAClientButton btnLaunch; private XNAClientButton btnCancel; private XNAClientButton btnReturn; private XNATextBlock tbMissionDescription; private XNATrackbar trbDifficultySelector; private List userSettings = new List(); private CheaterWindow cheaterWindow; public List CheckBoxes { get; } = new(); public List DropDowns { get; } = new(); private IniFile gameOptionsIni; private string[] filesToCheck = new string[] { "INI/AI.ini", "INI/AIE.ini", "INI/Art.ini", "INI/ArtE.ini", "INI/Enhance.ini", "INI/Rules.ini", "INI/Map Code/Difficulty Hard.ini", "INI/Map Code/Difficulty Medium.ini", "INI/Map Code/Difficulty Easy.ini" }; private Mission missionToLaunch; private List _allMissions = []; public IReadOnlyCollection AllMissions { get => _allMissions; } private Dictionary _uniqueIDToMissions = new(); public IReadOnlyDictionary UniqueIDToMissions => _uniqueIDToMissions; private void AddMission(Mission mission) { // no matter whether the key is duplicated, the mission is always added to AllMissions _allMissions.Add(mission); // but only the first mission is recorded in UniqueIDToMissions if (_uniqueIDToMissions.ContainsKey(mission.CustomMissionID)) { Logger.Log($"CampaignSelector: duplicated mission. CodeName: {mission.CodeName}. ID: {mission.CustomMissionID}. Description: {mission.UntranslatedGUIName}."); if (!string.IsNullOrEmpty(mission.Scenario)) mission.Enabled = false; } else { _uniqueIDToMissions.Add(mission.CustomMissionID, mission); } } public override void Initialize() { Name = "CampaignSelector"; BackgroundTexture = AssetLoader.LoadTexture("missionselectorbg.png"); ClientRectangle = new Rectangle(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT); BorderColor = UISettings.ActiveSettings.PanelBorderColor; gameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ClientConfiguration.GAME_OPTIONS)); var lblSelectCampaign = new XNALabel(WindowManager); lblSelectCampaign.Name = nameof(lblSelectCampaign); lblSelectCampaign.FontIndex = 1; lblSelectCampaign.ClientRectangle = new Rectangle(12, 12, 0, 0); lblSelectCampaign.Text = "MISSIONS:".L10N("Client:Main:Missions"); lbCampaignList = new XNAListBox(WindowManager); lbCampaignList.Name = nameof(lbCampaignList); lbCampaignList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2); lbCampaignList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbCampaignList.ClientRectangle = new Rectangle(12, lblSelectCampaign.Bottom + 6, 300, 516); lbCampaignList.SelectedIndexChanged += LbCampaignList_SelectedIndexChanged; var lblMissionDescriptionHeader = new XNALabel(WindowManager); lblMissionDescriptionHeader.Name = nameof(lblMissionDescriptionHeader); lblMissionDescriptionHeader.FontIndex = 1; lblMissionDescriptionHeader.ClientRectangle = new Rectangle( lbCampaignList.Right + 12, lblSelectCampaign.Y, 0, 0); lblMissionDescriptionHeader.Text = "MISSION DESCRIPTION:".L10N("Client:Main:MissionDescription"); tbMissionDescription = new XNATextBlock(WindowManager); tbMissionDescription.Name = nameof(tbMissionDescription); tbMissionDescription.ClientRectangle = new Rectangle( lblMissionDescriptionHeader.X, lblMissionDescriptionHeader.Bottom + 6, Width - 24 - lbCampaignList.Right, pnlMissionPreviewEnabled ? 430 - 200 - 12 : 430); tbMissionDescription.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; tbMissionDescription.Alpha = 1.0f; tbMissionDescription.BackgroundTexture = AssetLoader.CreateTexture(AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIBackgroundColor), tbMissionDescription.Width, tbMissionDescription.Height); var lblDifficultyLevel = new XNALabel(WindowManager); lblDifficultyLevel.Name = nameof(lblDifficultyLevel); lblDifficultyLevel.Text = "DIFFICULTY LEVEL".L10N("Client:Main:DifficultyLevel"); lblDifficultyLevel.FontIndex = 1; Vector2 textSize = Renderer.GetTextDimensions(lblDifficultyLevel.Text, lblDifficultyLevel.FontIndex); lblDifficultyLevel.ClientRectangle = new Rectangle( tbMissionDescription.X + (tbMissionDescription.Width - (int)textSize.X) / 2, tbMissionDescription.Bottom + 12, (int)textSize.X, (int)textSize.Y); trbDifficultySelector = new XNATrackbar(WindowManager); trbDifficultySelector.Name = nameof(trbDifficultySelector); trbDifficultySelector.ClientRectangle = new Rectangle( tbMissionDescription.X, lblDifficultyLevel.Bottom + 6, tbMissionDescription.Width, 30); trbDifficultySelector.MinValue = 0; trbDifficultySelector.MaxValue = 2; trbDifficultySelector.BackgroundTexture = AssetLoader.CreateTexture( new Color(0, 0, 0, 128), 2, 2); trbDifficultySelector.ButtonTexture = AssetLoader.LoadTextureUncached( "trackbarButton_difficulty.png"); var lblEasy = new XNALabel(WindowManager); lblEasy.Name = nameof(lblEasy); lblEasy.FontIndex = 1; lblEasy.Text = "EASY".L10N("Client:Main:DifficultyEasy"); lblEasy.ClientRectangle = new Rectangle(trbDifficultySelector.X, trbDifficultySelector.Bottom + 6, 1, 1); var lblNormal = new XNALabel(WindowManager); lblNormal.Name = nameof(lblNormal); lblNormal.FontIndex = 1; lblNormal.Text = "NORMAL".L10N("Client:Main:DifficultyNormal"); textSize = Renderer.GetTextDimensions(lblNormal.Text, lblNormal.FontIndex); lblNormal.ClientRectangle = new Rectangle( tbMissionDescription.X + (tbMissionDescription.Width - (int)textSize.X) / 2, lblEasy.Y, (int)textSize.X, (int)textSize.Y); var lblHard = new XNALabel(WindowManager); lblHard.Name = nameof(lblHard); lblHard.FontIndex = 1; lblHard.Text = "HARD".L10N("Client:Main:DifficultyHard"); lblHard.ClientRectangle = new Rectangle( tbMissionDescription.Right - lblHard.Width, lblEasy.Y, 1, 1); btnLaunch = new XNAClientButton(WindowManager); btnLaunch.Name = nameof(btnLaunch); btnLaunch.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLaunch.Text = "Launch".L10N("Client:Main:ButtonLaunch"); btnLaunch.AllowClick = false; btnLaunch.LeftClick += BtnLaunch_LeftClick; btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.ClientRectangle = new Rectangle(Width - 145, btnLaunch.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; if (pnlMissionPreviewEnabled) { pnlMissionPreview = new XNAPanel(WindowManager); pnlMissionPreview.Name = nameof(pnlMissionPreview); pnlMissionPreview.ClientRectangle = new Rectangle( tbMissionDescription.X, tbMissionDescription.Bottom + 12, tbMissionDescription.Width, 200); pnlMissionPreview.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; pnlMissionPreview.BackgroundTexture = CreateLetterboxedTexture(AssetLoader.LoadTextureUncached(defaultMissionPreviewPath), pnlMissionPreview.Width, pnlMissionPreview.Height); pnlMissionPreviewBackgroundTextureNeedsDispose = true; } else { pnlMissionPreview = null; } AddChild(lblSelectCampaign); AddChild(lblMissionDescriptionHeader); AddChild(lbCampaignList); AddChild(tbMissionDescription); AddChild(lblDifficultyLevel); AddChild(btnLaunch); AddChild(btnCancel); AddChild(trbDifficultySelector); AddChild(lblEasy); AddChild(lblNormal); AddChild(lblHard); if (pnlMissionPreview != null) AddChild(pnlMissionPreview); if (ClientConfiguration.Instance.CampaignTagSelectorEnabled) { btnReturn = new XNAClientButton(WindowManager); btnReturn.Name = nameof(btnReturn); btnReturn.ClientRectangle = new Rectangle(trbDifficultySelector.X, btnLaunch.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnReturn.Text = "Campaigns".L10N("Client:Main:ButtonReturnToCampaigns"); btnReturn.LeftClick += BtnReturn_LeftClick; btnReturn.Disable(); AddChild(btnReturn); } // Set control attributes from INI file base.Initialize(); // Center on screen CenterOnParent(); trbDifficultySelector.Value = UserINISettings.Instance.Difficulty; userSettings.AddRange(Children.OfType()); ReadMissionList(); cheaterWindow = new CheaterWindow(WindowManager); var dp = new DarkeningPanel(WindowManager); dp.AddChild(cheaterWindow); AddChild(dp); dp.CenterOnParent(); cheaterWindow.CenterOnParent(); cheaterWindow.YesClicked += CheaterWindow_YesClicked; cheaterWindow.Disable(); LoadSettings(); } private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e) { if (lbCampaignList.SelectedIndex == -1) { tbMissionDescription.Text = string.Empty; UpdateMissionPreview(string.Empty); btnLaunch.AllowClick = false; return; } Mission mission = selectedMissions[lbCampaignList.SelectedIndex]; UpdateMissionPreview(mission.PreviewImage); if (string.IsNullOrEmpty(mission.Scenario)) { tbMissionDescription.Text = string.Empty; btnLaunch.AllowClick = false; return; } tbMissionDescription.Text = mission.GUIDescription; if (!mission.Enabled) { btnLaunch.AllowClick = false; return; } btnLaunch.AllowClick = true; } // TODO: Modify XNAUI by adding PanelBackgroundImageDrawMode.LETTERBOXED as a new draw mode. private Texture2D CreateLetterboxedTexture(Texture2D sourceTexture, int targetWidth, int targetHeight, bool disposeSourceTexture = true) { // Calculate aspect ratios float sourceAspect = (float)sourceTexture.Width / sourceTexture.Height; float targetAspect = (float)targetWidth / targetHeight; int drawWidth, drawHeight, drawX, drawY; // Determine scaled dimensions while maintaining aspect ratio if (sourceAspect > targetAspect) { // Source is wider - fit to width drawWidth = targetWidth; drawHeight = (int)(targetWidth / sourceAspect); drawX = 0; drawY = (targetHeight - drawHeight) / 2; } else { // Source is taller - fit to height drawHeight = targetHeight; drawWidth = (int)(targetHeight * sourceAspect); drawX = (targetWidth - drawWidth) / 2; drawY = 0; } // Create the composite texture RenderTarget2D renderTarget = new RenderTarget2D( WindowManager.GraphicsDevice, targetWidth, targetHeight, false, SurfaceFormat.Color, DepthFormat.None); WindowManager.GraphicsDevice.SetRenderTarget(renderTarget); WindowManager.GraphicsDevice.Clear(AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIBackgroundColor)); var spriteBatch = new SpriteBatch(WindowManager.GraphicsDevice); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend); spriteBatch.Draw( sourceTexture, new Rectangle(drawX, drawY, drawWidth, drawHeight), Color.White); spriteBatch.End(); spriteBatch.Dispose(); WindowManager.GraphicsDevice.SetRenderTarget(null); if (disposeSourceTexture) sourceTexture.Dispose(); return renderTarget; } private void BtnCancel_LeftClick(object sender, EventArgs e) { SaveSettings(); Disable(); } private void BtnReturn_LeftClick(object sender, EventArgs e) { campaignTagSelector.NoFadeSwitch(); } private void BtnLaunch_LeftClick(object sender, EventArgs e) { SaveSettings(); int selectedMissionId = lbCampaignList.SelectedIndex; Mission mission = selectedMissions[selectedMissionId]; if (!ClientConfiguration.Instance.ModMode && (!Updater.IsFileNonexistantOrOriginal(mission.Scenario) || AreFilesModified())) { // Confront the user by showing the cheater screen missionToLaunch = mission; cheaterWindow.Enable(); return; } LaunchMission(mission); } private bool AreFilesModified() { foreach (string filePath in filesToCheck) { if (!Updater.IsFileNonexistantOrOriginal(filePath)) return true; } return false; } /// /// Called when the user wants to proceed to the mission despite having /// being called a cheater. /// private void CheaterWindow_YesClicked(object sender, EventArgs e) { LaunchMission(missionToLaunch); } /// /// Starts a singleplayer mission. /// private void LaunchMission(Mission mission) { CustomMissionHelper.CopySupplementalMissionFiles(mission); string scenario = mission.Scenario; FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); spawnerSettingsFile.Delete(); bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI; Logger.Log("About to write spawn.ini."); IniFile spawnIni = new(spawnerSettingsFile.FullName) { Comment = "Generated by CnCNet Client" }; IniSection spawnIniSettings = new("Settings"); if (copyMapsToSpawnmapINI) spawnIniSettings.AddKey("Scenario", "spawnmap.ini"); else spawnIniSettings.AddKey("Scenario", scenario); // No one wants to play missions on Fastest, so we'll change it to Faster if (UserINISettings.Instance.GameSpeed == 0) UserINISettings.Instance.GameSpeed.Value = 1; spawnIniSettings.AddKey("CampaignID", mission.CampaignID.ToString(CultureInfo.InvariantCulture)); spawnIniSettings.AddKey("GameSpeed", UserINISettings.Instance.GameSpeed.ToString()); switch (ClientConfiguration.Instance.ClientGameType) { case ClientType.YR or ClientType.Ares: spawnIniSettings.AddKey("Ra2Mode", (!mission.RequiredAddon).ToString(CultureInfo.InvariantCulture)); break; case ClientType.TS: spawnIniSettings.AddKey("Firestorm", mission.RequiredAddon.ToString(CultureInfo.InvariantCulture)); break; // TODO figure out the RA one } spawnIniSettings.AddKey("CustomLoadScreen", LoadingScreenController.GetLoadScreenName(mission.Side.ToString())); spawnIniSettings.AddKey("IsSinglePlayer", "Yes"); spawnIniSettings.AddKey("SidebarHack", ClientConfiguration.Instance.SidebarHack.ToString(CultureInfo.InvariantCulture)); spawnIniSettings.AddKey("Side", mission.Side.ToString(CultureInfo.InvariantCulture)); spawnIniSettings.AddKey("BuildOffAlly", mission.BuildOffAlly.ToString(CultureInfo.InvariantCulture)); UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value; spawnIniSettings.AddKey("DifficultyModeHuman", mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString(CultureInfo.InvariantCulture)); spawnIniSettings.AddKey("DifficultyModeComputer", GetComputerDifficulty().ToString(CultureInfo.InvariantCulture)); if (mission.IsCustomMission) { spawnIniSettings.AddKey("CustomMissionID", mission.CustomMissionID.ToString(CultureInfo.InvariantCulture)); } spawnIni.AddSection(spawnIniSettings); WriteMissionSectionToSpawnIni(spawnIni, mission); foreach (CampaignCheckBox chkBox in CheckBoxes) chkBox.ApplySpawnIniCode(spawnIni); foreach (CampaignDropDown dd in DropDowns) dd.ApplySpawnIniCode(spawnIni); // Apply forced options from GameOptions.ini List forcedKeys = gameOptionsIni.GetSectionKeys("CampaignForcedSpawnIniOptions"); if (forcedKeys != null) { foreach (string key in forcedKeys) { spawnIni.SetStringValue("Settings", key, gameOptionsIni.GetStringValue("CampaignForcedSpawnIniOptions", key, String.Empty)); } } spawnIni.WriteIniFile(); var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value])); string difficultyName = DifficultyNames[trbDifficultySelector.Value]; if (copyMapsToSpawnmapINI) { var mapIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario)); IniFile.ConsolidateIniFiles(mapIni, difficultyIni); foreach (CampaignCheckBox chkBox in CheckBoxes) chkBox.ApplyMapCode(mapIni, gameMode: null); foreach (CampaignDropDown dd in DropDowns) dd.ApplyMapCode(mapIni, gameMode: null); mapIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawnmap.ini")); } UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value; UserINISettings.Instance.SaveSettings(); if (ClientConfiguration.Instance.ReturnToMainMenuOnMissionLaunch) Disable(); else ToggleControls(false); discordHandler.UpdatePresence(mission.UntranslatedGUIName, difficultyName, mission.IconPath, true); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; GameProcessLogic.StartGameProcess(WindowManager); } public static void WriteMissionSectionToSpawnIni(IniFile spawnIni, Mission mission) { bool hasGameMissionData = false; string scenarioPath = SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario); if (!mission.IsCustomMission && File.Exists(scenarioPath)) { var mapIni = new IniFile(scenarioPath); mission.GameMissionConfigSection = mapIni.GetSection("GameMissionConfig"); if (mission.GameMissionConfigSection is not null) hasGameMissionData = true; } if (mission.IsCustomMission && mission.GameMissionConfigSection is not null || hasGameMissionData) { // copy an IniSection IniSection spawnIniMissionIniSection = new(mission.Scenario); string loadingScreenName = string.Empty; string loadingScreenPalName = string.Empty; foreach (var kvp in mission.GameMissionConfigSection.Keys) { if (string.IsNullOrEmpty(kvp.Value)) { if (kvp.Key.Equals("LS640BkgdName", StringComparison.InvariantCulture) || kvp.Key.Equals("LS800BkgdName", StringComparison.InvariantCulture)) loadingScreenName = kvp.Value; else if (kvp.Key.Equals("LS800BkgdPal", StringComparison.InvariantCulture)) loadingScreenPalName = kvp.Value; } spawnIniMissionIniSection.AddKey(kvp.Key, kvp.Value); } if (string.IsNullOrEmpty(loadingScreenName)) { string lsFilename = CustomMissionHelper.CustomMissionSupplementDefinition.FirstOrDefault(x => x.extension.Equals("shp", StringComparison.InvariantCultureIgnoreCase)).filename; if (!string.IsNullOrEmpty(lsFilename)) { spawnIniMissionIniSection.AddOrReplaceKey("LS640BkgdName", lsFilename); spawnIniMissionIniSection.AddOrReplaceKey("LS800BkgdName", lsFilename); } } if (string.IsNullOrEmpty(loadingScreenPalName)) { string palFilename = CustomMissionHelper.CustomMissionSupplementDefinition.FirstOrDefault(x => x.extension.Equals("pal", StringComparison.InvariantCultureIgnoreCase)).filename; if (!string.IsNullOrEmpty(palFilename)) spawnIniMissionIniSection.AddOrReplaceKey("LS800BkgdPal", palFilename); } // append the new IniSection spawnIni.AddSection(spawnIniMissionIniSection); spawnIni.SetStringValue("Settings", "ReadMissionSection", "Yes"); } } private void ToggleControls(bool enabled) { btnLaunch.AllowClick = enabled; btnCancel.AllowClick = enabled; lbCampaignList.Enabled = enabled; trbDifficultySelector.Enabled = enabled; if (btnReturn is not null) btnReturn.AllowClick = enabled; foreach (IUserSetting setting in userSettings) { if (setting is SettingCheckBoxBase cb) cb.AllowChecking = enabled; else if (setting is SettingDropDownBase dd) dd.AllowDropDown = enabled; } } private int GetComputerDifficulty() => Math.Abs(trbDifficultySelector.Value - 2); private void GameProcessExited_Callback() { WindowManager.AddCallback(new Action(GameProcessExited), null); } protected virtual void GameProcessExited() { GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; CustomMissionHelper.DeleteSupplementalMissionFiles(); // Logger.Log("GameProcessExited: Updating Discord Presence."); discordHandler.UpdatePresence(); if (!ClientConfiguration.Instance.ReturnToMainMenuOnMissionLaunch) ToggleControls(true); // Handle ResetToDefaultOnGameExit { // Reset campaign checkboxes foreach (CampaignCheckBox cb in CheckBoxes) { if (cb.ResetToDefaultOnGameExit) cb.ResetToDefault(); } // Reset user settings foreach (IUserSetting setting in userSettings) { if (!setting.ResetToDefaultOnGameExit) continue; if (setting is SettingCheckBoxBase cb) cb.Checked = cb.DefaultValue; else if (setting is SettingDropDownBase dd) dd.SelectedIndex = dd.DefaultValue; } SaveSettings(); } } private void ReadMissionList() { ParseBattleIni("INI/Battle.ini"); if (AllMissions.Count == 0) ParseBattleIni("INI/" + ClientConfiguration.Instance.BattleFSFileName); LoadCustomMissions(); LoadMissionsWithFilter(null, disableCustomMissions: true, disableOfficialMissions: false); } private void LoadCustomMissions() { string customMissionsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ClientConfiguration.Instance.CustomMissionPath); if (!Directory.Exists(customMissionsDirectory)) return; string[] mapFiles = Directory.GetFiles(customMissionsDirectory, "*.map"); if (mapFiles.Length == 0) return; foreach (string mapFilePath in mapFiles) { var mapFile = new IniFile(mapFilePath); IniSection clientMissionDataSection = mapFile.GetSection("ClientMissionConfig"); if (clientMissionDataSection is null) continue; IniSection? gameMissionDataSection = mapFile.GetSection("GameMissionConfig"); string filename = new FileInfo(mapFilePath).Name; string scenario = SafePath.CombineFilePath(ClientConfiguration.Instance.CustomMissionPath, filename); Mission mission = Mission.NewCustomMission(clientMissionDataSection, missionCodeName: filename, scenario, gameMissionDataSection); AddMission(mission); } } /// /// Parses a Battle(E).ini file. Returns true if succesful (file found), otherwise false. /// /// The path of the file, relative to the game directory. /// True if succesful, otherwise false. private bool ParseBattleIni(string path) { Logger.Log("Attempting to parse " + path + " to populate mission list."); FileInfo battleIniFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path); if (!battleIniFileInfo.Exists) { Logger.Log("File " + path + " not found. Ignoring."); return false; } if (selectedMissions.Count > 0) { throw new InvalidOperationException("Loading multiple Battle*.ini files is not supported anymore."); } var battleIni = new IniFile(battleIniFileInfo.FullName); List battleKeys = battleIni.GetSectionKeys("Battles"); if (battleKeys == null) return false; // File exists but [Battles] doesn't for (int i = 0; i < battleKeys.Count; i++) { string battleEntry = battleKeys[i]; string battleSection = battleIni.GetStringValue("Battles", battleEntry, "NOT FOUND"); if (!battleIni.SectionExists(battleSection)) continue; var mission = new Mission(battleIni.GetSection(battleSection), missionCodeName: battleEntry); AddMission(mission); } Logger.Log("Finished parsing " + path + "."); return true; } /// /// Load or re-load missons with selected tags. /// /// Missions with at lease one of which tags to be shown. As an exception, null means show all missions. /// True means show official missions. False means show custom missions. public void LoadMissionsWithFilter(ISet selectedTags, bool disableCustomMissions = true, bool disableOfficialMissions = false) { selectedMissions.Clear(); lbCampaignList.IsChangingSize = true; lbCampaignList.Clear(); lbCampaignList.SelectedIndex = -1; // The following two lines are handled by LbCampaignList_SelectedIndexChanged // tbMissionDescription.Text = string.Empty; // btnLaunch.AllowClick = false; // Select missions with the filter IEnumerable missions = AllMissions; if (disableCustomMissions && disableOfficialMissions) { // do nothing } else if (disableCustomMissions) { missions = missions.Where(mission => !mission.IsCustomMission); } else if (disableOfficialMissions) { missions = missions.Where(mission => mission.IsCustomMission); } else { // do nothing } if (selectedTags != null) missions = missions.Where(mission => mission.Tags.Intersect(selectedTags).Any()).ToList(); selectedMissions = missions.ToList(); // Update lbCampaignList with selected missions foreach (Mission mission in selectedMissions) { var item = new XNAListBoxItem(); item.Text = mission.GUIName; if (!mission.Enabled) { item.TextColor = UISettings.ActiveSettings.DisabledItemColor; } else if (string.IsNullOrEmpty(mission.Scenario)) { item.TextColor = AssetLoader.GetColorFromString( ClientConfiguration.Instance.ListBoxHeaderColor); item.IsHeader = true; item.Selectable = false; } else { item.TextColor = lbCampaignList.DefaultItemColor; } if (!string.IsNullOrEmpty(mission.IconPath)) item.Texture = AssetLoader.LoadTexture(mission.IconPath + "icon.png"); lbCampaignList.AddItem(item); } lbCampaignList.IsChangingSize = false; lbCampaignList.TopIndex = 0; } /// /// Saves settings to an INI file on the file system. /// private void SaveSettings() { SaveUserSettings(); SaveCampaignSettings(); } private void SaveUserSettings() { userSettings.ForEach(c => c.Save()); UserINISettings.Instance.SaveSettings(); } private void SaveCampaignSettings() { if (!ClientConfiguration.Instance.SaveCampaignGameOptions) return; try { FileInfo settingsFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH); settingsFileInfo.Delete(); var settingsIni = new IniFile(settingsFileInfo.FullName); foreach (CampaignDropDown dd in DropDowns) settingsIni.SetStringValue("GameOptions", dd.Name, dd.SelectedIndex.ToString()); foreach (CampaignCheckBox cb in CheckBoxes) settingsIni.SetStringValue("GameOptions", cb.Name, cb.Checked.ToString()); settingsIni.WriteIniFile(); } catch (Exception ex) { Logger.Log($"Saving campaign settings failed! Reason: {ex}"); } } /// /// Loads settings from an INI file on the file system. /// private void LoadSettings() { LoadUserSettings(); LoadCampaignSettings(); } private void LoadUserSettings() => userSettings.ForEach(c => c.Load()); private void LoadCampaignSettings() { if (!ClientConfiguration.Instance.SaveCampaignGameOptions) return; var settingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, SETTINGS_PATH)); foreach (CampaignDropDown dd in DropDowns) { dd.SelectedIndex = settingsIni.GetIntValue("GameOptions", dd.Name, dd.SelectedIndex); if (dd.SelectedIndex > -1 && dd.SelectedIndex < dd.Items.Count) dd.SelectedIndex = dd.SelectedIndex; } foreach (CampaignCheckBox cb in CheckBoxes) cb.Checked = settingsIni.GetBooleanValue("GameOptions", cb.Name, cb.Checked); } public override void Draw(GameTime gameTime) { base.Draw(gameTime); } private void UpdateMissionPreview(string missionPreviewFileName) { if (pnlMissionPreview == null) return; if (pnlMissionPreviewBackgroundTextureNeedsDispose) { Debug.Assert(pnlMissionPreview.BackgroundTexture != null, "Expected background texture to dispose, but it was null."); pnlMissionPreview.BackgroundTexture.Dispose(); pnlMissionPreview.BackgroundTexture = null; pnlMissionPreviewBackgroundTextureNeedsDispose = false; } string previewFilePath = null; if (!string.IsNullOrEmpty(missionPreviewFileName)) previewFilePath = SafePath.CombineFilePath(missionPreviewFolder, missionPreviewFileName); if (string.IsNullOrEmpty(missionPreviewFileName) || !File.Exists(previewFilePath)) { pnlMissionPreview.BackgroundTexture = CreateLetterboxedTexture( AssetLoader.LoadTextureUncached(defaultMissionPreviewPath), pnlMissionPreview.Width, pnlMissionPreview.Height); } else { pnlMissionPreview.BackgroundTexture = CreateLetterboxedTexture( AssetLoader.LoadTextureUncached(previewFilePath), pnlMissionPreview.Width, pnlMissionPreview.Height); } pnlMissionPreviewBackgroundTextureNeedsDispose = true; } } } ================================================ FILE: DXMainClient/DXGUI/Campaign/CampaignTagSelector.cs ================================================ using System; using System.Collections.Generic; using ClientCore; using ClientGUI; using DTAClient.Domain; using Microsoft.Xna.Framework; using Rampastring.XNAUI; namespace DTAClient.DXGUI.Campaign { public class CampaignTagSelector : INItializableWindow { private const int DEFAULT_WIDTH = 576; private const int DEFAULT_HEIGHT = 475; private DiscordHandler discordHandler; public CampaignTagSelector(WindowManager windowManager, DiscordHandler discordHandler) : base(windowManager) { this.discordHandler = discordHandler; } public IReadOnlyDictionary UniqueIDToMissions => CampaignSelector.UniqueIDToMissions; public IReadOnlyCollection AllMissions => CampaignSelector.AllMissions; protected XNAClientButton btnCancel; protected XNAClientButton btnShowAllMission; public override void Initialize() { CampaignSelector = new CampaignSelector(WindowManager, discordHandler, this); DarkeningPanel.AddAndInitializeWithControl(WindowManager, CampaignSelector); CampaignSelector.Disable(); Name = nameof(CampaignTagSelector); if (!ClientConfiguration.Instance.CampaignTagSelectorEnabled) return; ClientRectangle = new Rectangle(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT); BorderColor = UISettings.ActiveSettings.PanelBorderColor; base.Initialize(); WindowManager.CenterControlOnScreen(this); btnCancel = FindChild(nameof(btnCancel)); btnCancel.LeftClick += BtnCancel_LeftClick; btnShowAllMission = FindChild(nameof(btnShowAllMission)); btnShowAllMission.LeftClick += (sender, e) => { CampaignSelector.LoadMissionsWithFilter(null, disableCustomMissions: false, disableOfficialMissions: false); NoFadeSwitch(); }; const string TagButtonsPrefix = "ButtonTag_"; var tagButtons = FindChildrenStartWith(TagButtonsPrefix); foreach (var tagButton in tagButtons) { if (tagButton.Enabled) { string tagName = tagButton.Name.Substring(TagButtonsPrefix.Length); tagButton.LeftClick += (sender, e) => { CampaignSelector.LoadMissionsWithFilter(new HashSet() { tagName }, disableCustomMissions: false, disableOfficialMissions: false); NoFadeSwitch(); }; } else { tagButton.AllowClick = false; } } } private void BtnCancel_LeftClick(object sender, EventArgs e) { Disable(); } public void Open() { if (ClientConfiguration.Instance.CampaignTagSelectorEnabled) Enable(); else CampaignSelector.Enable(); } public void NoFadeSwitch() { var dp = CampaignSelector.Parent as DarkeningPanel; dp?.ToggleFade(false); if (Visible) CampaignSelector.Enable(); else CampaignSelector.Disable(); dp?.ToggleFade(true); dp = Parent as DarkeningPanel; dp?.ToggleFade(false); if (Visible) Disable(); else Enable(); dp?.ToggleFade(true); } private CampaignSelector CampaignSelector; } } ================================================ FILE: DXMainClient/DXGUI/Campaign/CheaterWindow.cs ================================================ using System; using ClientCore.Extensions; using ClientGUI; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Campaign { public class CheaterWindow : XNAWindow { public CheaterWindow(WindowManager windowManager) : base(windowManager) { } public event EventHandler YesClicked; public override void Initialize() { Name = "CheaterScreen"; ClientRectangle = new Rectangle(0, 0, 334, 453); BackgroundTexture = AssetLoader.LoadTexture("cheaterbg.png"); var lblCheater = new XNALabel(WindowManager); lblCheater.Name = nameof(lblCheater); lblCheater.ClientRectangle = new Rectangle(0, 0, 0, 0); lblCheater.FontIndex = 1; lblCheater.Text = "CHEATER!".L10N("Client:Main:Cheater"); var lblDescription = new XNALabel(WindowManager); lblDescription.Name = nameof(lblDescription); lblDescription.ClientRectangle = new Rectangle(12, 40, 0, 0); lblDescription.Text = ("Modified game files have been detected. They could affect\n" + "the game experience.\n\n" + "Do you really lack the skill for winning the mission without\ncheating?").L10N("Client:Main:CheaterText"); var imagePanel = new XNAPanel(WindowManager); imagePanel.Name = nameof(imagePanel); imagePanel.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; imagePanel.ClientRectangle = new Rectangle(lblDescription.X, lblDescription.Bottom + 12, Width - 24, Height - (lblDescription.Bottom + 59)); imagePanel.BackgroundTexture = AssetLoader.LoadTextureUncached("cheater.png"); var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.ClientRectangle = new Rectangle(Width - 104, Height - 35, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; var btnYes = new XNAClientButton(WindowManager); btnYes.Name = nameof(btnYes); btnYes.ClientRectangle = new Rectangle(12, btnCancel.Y, btnCancel.Width, btnCancel.Height); btnYes.Text = "Yes".L10N("Client:Main:ButtonYes"); btnYes.LeftClick += BtnYes_LeftClick; AddChild(lblCheater); AddChild(lblDescription); AddChild(imagePanel); AddChild(btnCancel); AddChild(btnYes); lblCheater.CenterOnParent(); lblCheater.ClientRectangle = new Rectangle(lblCheater.X, 12, lblCheater.Width, lblCheater.Height); base.Initialize(); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Disable(); } private void BtnYes_LeftClick(object sender, EventArgs e) { Disable(); YesClicked?.Invoke(this, EventArgs.Empty); } } } ================================================ FILE: DXMainClient/DXGUI/GameClass.cs ================================================ using ClientCore; using ClientGUI; using ClientGUI.IME; using DTAClient.Domain; using DTAClient.DXGUI.Generic; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using System; using System.Buffers.Binary; using System.Diagnostics; using System.IO; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Campaign; using DTAClient.DXGUI.Multiplayer; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; using DTAClient.Online; using ClientGUI.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Rampastring.XNAUI.XNAControls; using MainMenu = DTAClient.DXGUI.Generic.MainMenu; using System.Threading.Tasks; #if WINFORMS using System.Windows.Forms; #endif namespace DTAClient.DXGUI { /// /// The main class for the game. Sets up asset search paths /// and initializes components. /// public class GameClass : Game { public GameClass() { graphics = new GraphicsDeviceManager(this); graphics.SynchronizeWithVerticalRetrace = false; #if !XNA graphics.HardwareModeSwitch = false; // Enable HiDef on a large monitor. if (!ScreenResolution.HiDefLimitResolution.Fits(ScreenResolution.DesktopResolution)) { // Enabling HiDef profile drops legacy GPUs not supporting DirectX 10. // In practice, it's recommended to have a DirectX 11 capable GPU. graphics.GraphicsProfile = GraphicsProfile.HiDef; } #endif content = new ContentManager(Services); } private static GraphicsDeviceManager graphics; ContentManager content; protected override void Initialize() { Logger.Log("Initializing GameClass."); string windowTitle = ClientConfiguration.Instance.WindowTitle; Window.Title = string.IsNullOrEmpty(windowTitle) ? string.Format("{0} Client", MainClientConstants.GAME_NAME_SHORT) : windowTitle; { string developBuildTitle = "Development Build".L10N("Client:Main:DevelopmentBuildTitle"); #if DEVELOPMENT_BUILD if (ClientConfiguration.Instance.ShowDevelopmentBuildWarnings) Window.Title += $" ({developBuildTitle})"; #endif } base.Initialize(); AssetLoader.Initialize(GraphicsDevice, content); AssetLoader.AssetSearchPaths.Add(UserINISettings.Instance.TranslationThemeFolderPath); AssetLoader.AssetSearchPaths.Add(ProgramConstants.GetResourcePath()); AssetLoader.AssetSearchPaths.Add(UserINISettings.Instance.TranslationFolderPath); AssetLoader.AssetSearchPaths.Add(ProgramConstants.GetBaseResourcePath()); AssetLoader.AssetSearchPaths.Add(ProgramConstants.GamePath); #if DX || (GL && WINFORMS) // Try to create and load a texture to check for MonoGame compatibility #if DX const string startupFailureFile = ".dxfail"; #elif GL && WINFORMS const string startupFailureFile = ".oglfail"; #endif try { Texture2D texture = new Texture2D(GraphicsDevice, 10, 10, false, SurfaceFormat.Color); Color[] colorArray = new Color[10 * 10]; texture.SetData(colorArray); _ = AssetLoader.LoadTextureUncached("checkBoxClear.png"); } catch (Exception ex) { // TODO Get English exception message if (ex.Message.Contains("DeviceRemoved")) { Logger.Log($"Creating texture on startup failed! Creating {startupFailureFile} file and re-launching client launcher."); DirectoryInfo clientDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); if (!clientDirectory.Exists) clientDirectory.Create(); // Create startup failure file that the launcher can check for this error // and handle it by redirecting the user to another version instead File.WriteAllBytes(SafePath.CombineFilePath(clientDirectory.FullName, startupFailureFile), new byte[] { 1 }); string launcherExe = ClientConfiguration.Instance.LauncherExe; if (string.IsNullOrEmpty(launcherExe)) { // LauncherExe is unspecified, just throw the exception forward // because we can't handle it Logger.Log("No LauncherExe= specified in ClientDefinitions.ini! " + "Forwarding exception to regular exception handler."); throw; } else { Logger.Log("Starting " + launcherExe + " and exiting."); Process.Start(SafePath.CombineFilePath(ProgramConstants.GamePath, launcherExe)); Environment.Exit(1); } } } #endif InitializeUISettings(); WindowManager wm = new(this, graphics); wm.Initialize(content, ProgramConstants.GetBaseResourcePath()); IServiceProvider serviceProvider = null; Task buildServiceProviderTask = Task.Run(() => { serviceProvider = BuildServiceProvider(wm); }); IMEHandler imeHandler = IMEHandler.Create(this); wm.IMEHandler = imeHandler; wm.ControlINIAttributeParsers.Add(new TranslationINIParser()); SetGraphicsMode(wm); #if WINFORMS wm.SetIcon(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "clienticon.ico")); wm.SetControlBox(true); // Enable resizable window for non-borderless windowed client, if integer scaling is enabled if (!UserINISettings.Instance.BorderlessWindowedClient && UserINISettings.Instance.IntegerScaledClient) { wm.SetFormBorderStyle(FormBorderStyle.Sizable); wm.SetMaximizeBox(true); //// Automatically update render resolution when the window size changes //// Disabled for now. It does not work as expected. //// To fix this, we need to make every window and control to be able to handle window size changes. //// This is not a trivial work and does not gain much benefit since the minimum render resolution and the maximum one are close. //// Example: https://github.com/Rampastring/WorldAlteringEditor/blob/71d9bd0ed9b9843d5dc15de14005f86b18e5465c/src/TSMapEditor/UI/Controls/INItializableWindow.cs#L98 //ScreenResolution lastWindowSizeCaptured = new(wm.Game.Window.ClientBounds); //wm.WindowSizeChangedByUser += (sender, e) => //{ // ScreenResolution currentWindowSize = new(wm.Game.Window.ClientBounds); // if (currentWindowSize != lastWindowSizeCaptured) // { // Logger.Log($"Window size changed from {lastWindowSizeCaptured} to {currentWindowSize}."); // lastWindowSizeCaptured = currentWindowSize; // SetGraphicsMode(wm, currentWindowSize.Width, currentWindowSize.Height, centerOnScreen: false); // } //}; wm.WindowSizeChangedByUser += (sender, e) => { imeHandler.SetIMETextInputRectangle(wm); }; } #endif wm.Cursor.Textures = new Texture2D[] { AssetLoader.LoadTexture("cursor.png"), AssetLoader.LoadTexture("waitCursor.png") }; #if WINFORMS FileInfo primaryNativeCursorPath = SafePath.GetFile(ProgramConstants.GetResourcePath(), "cursor.cur"); FileInfo alternativeNativeCursorPath = SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), "cursor.cur"); if (primaryNativeCursorPath.Exists) wm.Cursor.LoadNativeCursor(primaryNativeCursorPath.FullName); else if (alternativeNativeCursorPath.Exists) wm.Cursor.LoadNativeCursor(alternativeNativeCursorPath.FullName); #endif Components.Add(wm); string playerName = UserINISettings.Instance.PlayerName.Value.Trim(); if (UserINISettings.Instance.AutoRemoveUnderscoresFromName) { while (playerName.EndsWith("_")) playerName = playerName.Substring(0, playerName.Length - 1); } if (string.IsNullOrEmpty(playerName)) { playerName = Environment.UserName; playerName = playerName.Substring(playerName.IndexOf("\\") + 1); } playerName = Renderer.GetSafeString(NameValidator.GetValidOfflineName(playerName), 0); ProgramConstants.PLAYERNAME = playerName; UserINISettings.Instance.PlayerName.Value = playerName; buildServiceProviderTask.GetAwaiter().GetResult(); Logger.Log("Initializing loading screen."); LoadingScreen ls = serviceProvider.GetService(); wm.AddAndInitializeControl(ls); ls.ClientRectangle = new Rectangle((wm.RenderResolutionX - ls.Width) / 2, (wm.RenderResolutionY - ls.Height) / 2, ls.Width, ls.Height); } private static Random GetRandom() { var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); byte[] intBytes = new byte[sizeof(int)]; rng.GetBytes(intBytes); int seed = BinaryPrimitives.ReadInt32LittleEndian(intBytes); return new Random(seed); } private IServiceProvider BuildServiceProvider(WindowManager windowManager) { // Create host - this allows for things like DependencyInjection IHost host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { // services (or service-like) services .AddSingleton() .AddSingleton(windowManager) .AddSingleton(GraphicsDevice) .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(GetRandom()) .AddSingleton(); // singleton xna controls - same instance on each request services .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl() .AddSingletonXnaControl(); // transient xna controls - new instance on each request services .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl() .AddTransientXnaControl(); } ) .Build(); return host.Services.GetService(); } private void InitializeUISettings() { UISettings settings = new UISettings(); settings.AltColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIColor); settings.SubtleTextColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.UIHintTextColor); settings.ButtonTextColor = settings.AltColor; settings.ButtonHoverColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ButtonHoverColor); settings.TextColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.UILabelColor); //settings.WindowBorderColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.WindowBorderColor); settings.PanelBorderColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.PanelBorderColor); settings.BackgroundColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AltUIBackgroundColor); settings.FocusColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ListBoxFocusColor); settings.DisabledItemColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.DisabledButtonColor); settings.DefaultAlphaRate = ClientConfiguration.Instance.DefaultAlphaRate; settings.CheckBoxAlphaRate = ClientConfiguration.Instance.CheckBoxAlphaRate; settings.IndicatorAlphaRate = ClientConfiguration.Instance.IndicatorAlphaRate; settings.CheckBoxClearTexture = AssetLoader.LoadTexture("checkBoxClear.png"); settings.CheckBoxCheckedTexture = AssetLoader.LoadTexture("checkBoxChecked.png"); settings.CheckBoxDisabledClearTexture = AssetLoader.LoadTexture("checkBoxClearD.png"); settings.CheckBoxDisabledCheckedTexture = AssetLoader.LoadTexture("checkBoxCheckedD.png"); XNAPlayerSlotIndicator.LoadTextures(); UISettings.ActiveSettings = settings; } /// /// Sets the client's graphics mode. /// TODO move to some helper class? /// /// The window manager /// Whether to center the client window on the screen public static void SetGraphicsMode(WindowManager wm, bool centerOnScreen = true) { int windowWidth = UserINISettings.Instance.ClientResolutionX; int windowHeight = UserINISettings.Instance.ClientResolutionY; SetGraphicsMode(wm, windowWidth, windowHeight, centerOnScreen); } /// /// The viewport width /// The viewport height public static void SetGraphicsMode(WindowManager wm, int windowWidth, int windowHeight, bool centerOnScreen = true) { bool borderlessWindowedClient = UserINISettings.Instance.BorderlessWindowedClient; bool integerScale = UserINISettings.Instance.IntegerScaledClient; SetGraphicsMode(wm, windowWidth, windowHeight, borderlessWindowedClient, integerScale, centerOnScreen); } /// /// Whether to use borderless windowed mode /// Whether to use integer scaling public static void SetGraphicsMode(WindowManager wm, int windowWidth, int windowHeight, bool borderlessWindowedClient, bool integerScale, bool centerOnScreen = true) { var clientConfiguration = ClientConfiguration.Instance; (int desktopWidth, int desktopHeight) = ScreenResolution.SafeMaximumResolution; if (desktopWidth >= windowWidth && desktopHeight >= windowHeight) { if (!wm.InitGraphicsMode(windowWidth, windowHeight, false)) throw new GraphicsModeInitializationException("Setting graphics mode failed!".L10N("Client:Main:SettingGraphicModeFailed") + " " + windowWidth + "x" + windowHeight); } else { // fallback to the minimum supported resolution when the desktop is not sufficient to contain the client // e.g., when users set a lower desktop resolution but the client resolution in the settings file remains high if (!wm.InitGraphicsMode(1024, 600, false)) throw new GraphicsModeInitializationException("Setting default graphics mode failed!".L10N("Client:Main:SettingDefaultGraphicModeFailed")); } int renderResolutionX = 0; int renderResolutionY = 0; if (!integerScale || windowWidth < clientConfiguration.MinimumRenderWidth || windowHeight < clientConfiguration.MinimumRenderHeight) { int initialXRes = Math.Max(windowWidth, clientConfiguration.MinimumRenderWidth); initialXRes = Math.Min(initialXRes, clientConfiguration.MaximumRenderWidth); int initialYRes = Math.Max(windowHeight, clientConfiguration.MinimumRenderHeight); initialYRes = Math.Min(initialYRes, clientConfiguration.MaximumRenderHeight); double xRatio = (windowWidth) / (double)initialXRes; double yRatio = (windowHeight) / (double)initialYRes; double ratio = xRatio > yRatio ? yRatio : xRatio; // Special rule for 1360x768 and 1366x768 if ((windowWidth == 1366 || windowWidth == 1360) && windowHeight == 768) { // Most client interface has been designed for 1280x720 or 1280x800. // 1280x720 upscaled to 1366x768 doesn't look great, so we allow players with 1366x768 to use their native resolution with small black bars on the sides // This behavior is enforced even if IntegerScaledClient is turned off. renderResolutionX = windowWidth; renderResolutionY = windowHeight; } // Special rule: if 1280x720 is a valid render resolution, we allow 1.5x scaling for 1920x1080. if (windowWidth == 1920 && windowHeight == 1080 && 1280 >= clientConfiguration.MinimumRenderWidth && 1280 <= clientConfiguration.MaximumRenderWidth && 720 >= clientConfiguration.MinimumRenderHeight && 720 <= clientConfiguration.MaximumRenderHeight) { renderResolutionX = 1280; renderResolutionY = 720; } // Special rule: if 1280x800 is a valid render resolution, we allow 1.5x scaling for 1920x1200. if (windowWidth == 1920 && windowHeight == 1200 && 1280 >= clientConfiguration.MinimumRenderWidth && 1280 <= clientConfiguration.MaximumRenderWidth && 800 >= clientConfiguration.MinimumRenderHeight && 800 <= clientConfiguration.MaximumRenderHeight) { renderResolutionX = 1280; renderResolutionY = 800; } // Check whether we could integer-scale our client window if (ratio > 1.0) { for (int i = 2; i <= ScreenResolution.MAX_INT_SCALE; i++) { int sharpScaleRenderResX = windowWidth / i; int sharpScaleRenderResY = windowHeight / i; if (sharpScaleRenderResX >= clientConfiguration.MinimumRenderWidth && sharpScaleRenderResX <= clientConfiguration.MaximumRenderWidth && sharpScaleRenderResY >= clientConfiguration.MinimumRenderHeight && sharpScaleRenderResY <= clientConfiguration.MaximumRenderHeight) { renderResolutionX = sharpScaleRenderResX; renderResolutionY = sharpScaleRenderResY; break; } } } // No special rules are triggered. Just zoom the client to the window size with minimal black bars. if (renderResolutionX == 0 || renderResolutionY == 0) { renderResolutionX = initialXRes; renderResolutionY = initialYRes; if (ratio == xRatio) renderResolutionY = (int)(windowHeight / ratio); } } else { // Compute integer scale ratio using minimum render resolution // Note: this means we prefer larger scale ratio than render resolution. // This policy works best when maximum and minimum render resolution are close. int xScale = windowWidth / clientConfiguration.MinimumRenderWidth; int yScale = windowHeight / clientConfiguration.MinimumRenderHeight; int scale = Math.Min(xScale, yScale); // Compute render resolution renderResolutionX = Math.Min(clientConfiguration.MaximumRenderWidth, clientConfiguration.MinimumRenderWidth + (windowWidth - clientConfiguration.MinimumRenderWidth * scale) / scale); renderResolutionY = Math.Min(clientConfiguration.MaximumRenderHeight, clientConfiguration.MinimumRenderHeight + (windowHeight - clientConfiguration.MinimumRenderHeight * scale) / scale); } wm.SetBorderlessMode(borderlessWindowedClient); #if !XNA if (borderlessWindowedClient) { // Note: on fullscreen mode, the client resolution must exactly match the desktop resolution. Otherwise buttons outside of client resolution are unclickable. ScreenResolution clientResolution = (windowWidth, windowHeight); if (ScreenResolution.DesktopResolution == clientResolution) { Logger.Log($"Entering fullscreen mode with resolution {ScreenResolution.DesktopResolution}."); graphics.IsFullScreen = true; graphics.ApplyChanges(); } else { Logger.Log($"Not entering fullscreen mode due to resolution mismatch. Desktop: {ScreenResolution.DesktopResolution}, Client: {clientResolution}."); } } #endif if (centerOnScreen) wm.CenterOnScreen(); Logger.Log("Setting render resolution to " + renderResolutionX + "x" + renderResolutionY + ". Integer scaling: " + integerScale); wm.IntegerScalingOnly = integerScale; wm.SetRenderResolution(renderResolutionX, renderResolutionY); } } /// /// An exception that is thrown when initializing display / graphics mode fails. /// class GraphicsModeInitializationException : Exception { public GraphicsModeInitializationException(string message) : base(message) { } } } ================================================ FILE: DXMainClient/DXGUI/Generic/DropDownDataWriteMode.cs ================================================ namespace DTAClient.DXGUI.Generic { /// /// An enum for controlling how the game lobbies' /// drop-down controls' data should be written into the spawn INI. /// public enum DropDownDataWriteMode { /// /// The 0-based selected index of the drop-down control will /// be written into the INI. /// INDEX, /// /// If index 0 is selected, "false" will be written. /// Otherwise the client will write "true". /// BOOLEAN, /// /// The dropdown value displayed in the UI will /// be written into the INI. /// STRING, /// /// The dropdown value is filename of a mapcode INI file, which will be applied to the map. /// Nothing is written to spawn INI. /// MAPCODE } } ================================================ FILE: DXMainClient/DXGUI/Generic/ExtrasWindow.cs ================================================ using ClientCore; using ClientGUI; using DTAClient.Domain; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.Tools; using System; using System.Diagnostics; namespace DTAClient.DXGUI.Generic { public class ExtrasWindow : XNAWindow { private StatisticsWindow statisticsWindow; public ExtrasWindow(WindowManager windowManager, StatisticsWindow statisticsWindow) : base(windowManager) { this.statisticsWindow = statisticsWindow; } public override void Initialize() { Name = "ExtrasWindow"; ClientRectangle = new Rectangle(0, 0, 284, 190); BackgroundTexture = AssetLoader.LoadTexture("extrasMenu.png"); var btnExStatistics = new XNAClientButton(WindowManager); btnExStatistics.Name = nameof(btnExStatistics); btnExStatistics.ClientRectangle = new Rectangle(76, 17, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnExStatistics.Text = "Statistics".L10N("Client:Main:Statistics"); btnExStatistics.LeftClick += BtnExStatistics_LeftClick; var btnExMapEditor = new XNAClientButton(WindowManager); btnExMapEditor.Name = nameof(btnExMapEditor); btnExMapEditor.ClientRectangle = new Rectangle(76, 59, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnExMapEditor.Text = "Map Editor".L10N("Client:Main:MapEditor"); btnExMapEditor.LeftClick += BtnExMapEditor_LeftClick; var btnExCredits = new XNAClientButton(WindowManager); btnExCredits.Name = nameof(btnExCredits); btnExCredits.ClientRectangle = new Rectangle(76, 101, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnExCredits.Text = "Credits".L10N("Client:Main:Credits"); btnExCredits.LeftClick += BtnExCredits_LeftClick; var btnExCancel = new XNAClientButton(WindowManager); btnExCancel.Name = nameof(btnExCancel); btnExCancel.ClientRectangle = new Rectangle(76, 160, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnExCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnExCancel.LeftClick += BtnExCancel_LeftClick; AddChild(btnExStatistics); AddChild(btnExMapEditor); AddChild(btnExCredits); AddChild(btnExCancel); base.Initialize(); CenterOnParent(); } private void BtnExStatistics_LeftClick(object sender, EventArgs e) { Disable(); statisticsWindow.Enable(); } private void BtnExMapEditor_LeftClick(object sender, EventArgs e) { OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); using var mapEditorProcess = new Process(); if (osVersion != OSVersion.UNIX) mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MapEditorExePath); else mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.UnixMapEditorExePath); mapEditorProcess.StartInfo.UseShellExecute = false; mapEditorProcess.Start(); Disable(); } private void BtnExCredits_LeftClick(object sender, EventArgs e) { ProcessLauncher.StartShellProcess(ClientConfiguration.Instance.CreditsURL); } private void BtnExCancel_LeftClick(object sender, EventArgs e) { Disable(); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/GameInProgressWindow.cs ================================================ using Rampastring.XNAUI.XNAControls; using Rampastring.Tools; using System; using ClientCore; using Rampastring.XNAUI; using ClientGUI; using System.IO; using ClientCore.Extensions; using ClientCore.Enums; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; using SixLabors.ImageSharp; namespace DTAClient.DXGUI { /// /// Displays a dialog in the client when a game is in progress. /// Also enables power-saving (lowers FPS) while a game is in progress, /// and performs various operations on game start and exit. /// public class GameInProgressWindow : XNAPanel { private const double POWER_SAVING_FPS = 5.0; public GameInProgressWindow(WindowManager windowManager) : base(windowManager) { } private bool initialized = false; private bool nativeCursorUsed = false; private List debugSnapshotDirectories; private DateTime debugLogLastWriteTime; private bool deletingLogFilesFailed = false; public override void Initialize() { if (initialized) throw new InvalidOperationException("GameInProgressWindow cannot be initialized twice!"); initialized = true; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; DrawBorders = false; ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY); XNAWindow window = new XNAWindow(WindowManager); window.Name = "GameInProgressWindow"; window.BackgroundTexture = AssetLoader.LoadTexture("gameinprogresswindowbg.png"); window.ClientRectangle = new Rectangle(0, 0, 200, 100); XNALabel explanation = new XNALabel(WindowManager); explanation.Text = "A game is in progress.".L10N("Client:Main:GameInProgress"); AddChild(window); window.AddChild(explanation); base.Initialize(); GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted; GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; explanation.CenterOnParent(); window.CenterOnParent(); Game.TargetElapsedTime = TimeSpan.FromMilliseconds(1000.0 / UserINISettings.Instance.ClientFPS); Visible = false; Enabled = false; if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) { try { FileInfo debugLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "debug", "debug.log"); if (debugLogFileInfo.Exists) debugLogLastWriteTime = debugLogFileInfo.LastWriteTime; } catch { } } } private void SharedUILogic_GameProcessStarted() { if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) { debugSnapshotDirectories = GetAllDebugSnapshotDirectories(); } else { try { SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "EXCEPT.TXT"); for (int i = 0; i < 8; i++) SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "SYNC" + i + ".TXT"); deletingLogFilesFailed = false; } catch (Exception ex) { Logger.Log("Exception when deleting error log files! Message: " + ex.ToString()); deletingLogFilesFailed = true; } } Visible = true; Enabled = true; WindowManager.Cursor.Visible = false; nativeCursorUsed = Game.IsMouseVisible; Game.IsMouseVisible = false; ProgramConstants.IsInGame = true; Game.TargetElapsedTime = TimeSpan.FromMilliseconds(1000.0 / POWER_SAVING_FPS); #if WINFORMS if (UserINISettings.Instance.MinimizeWindowsOnGameStart) WindowManager.MinimizeWindow(); #endif } private void SharedUILogic_GameProcessExited() { WindowManager.AddCallback(new Action(HandleGameProcessExited), null); } private void HandleGameProcessExited() { Visible = false; Enabled = false; if (nativeCursorUsed) Game.IsMouseVisible = true; else WindowManager.Cursor.Visible = true; ProgramConstants.IsInGame = false; Game.TargetElapsedTime = TimeSpan.FromMilliseconds(1000.0 / UserINISettings.Instance.ClientFPS); #if WINFORMS if (UserINISettings.Instance.MinimizeWindowsOnGameStart) WindowManager.MaximizeWindow(); #endif UserINISettings.Instance.ReloadSettings(); if (UserINISettings.Instance.BorderlessWindowedClient) { // Hack: Re-set graphics mode // Windows resizes our window if we're in fullscreen mode and // the in-game resolution is lower than the user's desktop resolution. // After the game exits, Windows doesn't properly re-size our window // back to cover the entire screen, which causes graphics to get // stretched and also messes up input handling since the window manager // still thinks it's using the original resolution. // Re-setting the graphics mode fixes it. GameClass.SetGraphicsMode(WindowManager); } DateTime dtn = DateTime.Now; if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) { Task.Run(ProcessScreenshots); // TODO: Ares debug log handling should be addressed in Ares DLL itself. // For now the following are handled here: // 1. Make a copy of syringe.log in debug snapshot directory on both crash and desync. // 2. Move SYNCX.txt from game directory to debug snapshot directory on desync. // 3. Make a debug snapshot directory & copy debug.log to it on desync even if full crash dump wasn't created. // 4. Handle the empty snapshot directories created on a crash if debug logging was disabled. string snapshotDirectory = GetNewestDebugSnapshotDirectory(); bool snapshotCreated = snapshotDirectory != null; snapshotDirectory = snapshotDirectory ?? SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "debug", FormattableString.Invariant($"snapshot-{dtn.ToString("yyyyMMdd-HHmmss")}")); bool debugLogModified = false; FileInfo debugLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "debug", "debug.log"); DateTime lastWriteTime = new DateTime(); if (debugLogFileInfo.Exists) lastWriteTime = debugLogFileInfo.LastAccessTime; if (!lastWriteTime.Equals(debugLogLastWriteTime)) { debugLogModified = true; debugLogLastWriteTime = lastWriteTime; } if (CopySyncErrorLogs(snapshotDirectory, null) || snapshotCreated) { FileInfo snapShotDebugLogFileInfo = SafePath.GetFile(snapshotDirectory, "debug.log"); if (debugLogFileInfo.Exists && !snapShotDebugLogFileInfo.Exists && debugLogModified) File.Copy(debugLogFileInfo.FullName, snapShotDebugLogFileInfo.FullName); CopyErrorLog(snapshotDirectory, "syringe.log", null); } } else { if (deletingLogFilesFailed) return; CopyErrorLog(SafePath.CombineDirectoryPath(ProgramConstants.ClientUserFilesPath, "GameCrashLogs"), "EXCEPT.TXT", dtn); CopySyncErrorLogs(SafePath.CombineDirectoryPath(ProgramConstants.ClientUserFilesPath, "SyncErrorLogs"), dtn); } } /// /// Attempts to copy a general error log from game directory to another directory. /// /// Directory to copy error log to. /// Filename of the error log. /// Time to to apply as a timestamp to filename. Set to null to not apply a timestamp. /// True if error log was copied, false otherwise. private bool CopyErrorLog(string directory, string filename, DateTime? dateTime) { bool copied = false; try { FileInfo errorLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, filename); if (errorLogFileInfo.Exists) { DirectoryInfo errorLogDirectoryInfo = SafePath.GetDirectory(directory); if (!errorLogDirectoryInfo.Exists) errorLogDirectoryInfo.Create(); Logger.Log("The game crashed! Copying " + filename + " file."); string timeStamp = dateTime.HasValue ? dateTime.Value.ToString("_yyyy_MM_dd_HH_mm") : ""; string filenameCopy = Path.GetFileNameWithoutExtension(filename) + timeStamp + Path.GetExtension(filename); File.Copy(errorLogFileInfo.FullName, SafePath.CombineFilePath(directory, filenameCopy)); copied = true; } } catch (Exception ex) { Logger.Log("An error occured while checking for " + filename + " file. Message: " + ex.ToString()); } return copied; } /// /// Attempts to copy sync error logs from game directory to another directory. /// /// Directory to copy sync error logs to. /// Time to to apply as a timestamp to filename. Set to null to not apply a timestamp. /// True if any sync logs were copied, false otherwise. private bool CopySyncErrorLogs(string directory, DateTime? dateTime) { bool copied = false; try { for (int i = 0; i < 8; i++) { string filename = "SYNC" + i + ".TXT"; FileInfo syncErrorLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, filename); if (syncErrorLogFileInfo.Exists) { DirectoryInfo syncErrorLogDirectoryInfo = SafePath.GetDirectory(directory); if (!syncErrorLogDirectoryInfo.Exists) syncErrorLogDirectoryInfo.Create(); Logger.Log("There was a sync error! Copying file " + filename); string timeStamp = dateTime.HasValue ? dateTime.Value.ToString("_yyyy_MM_dd_HH_mm") : ""; string filenameCopy = Path.GetFileNameWithoutExtension(filename) + timeStamp + Path.GetExtension(filename); File.Copy(syncErrorLogFileInfo.FullName, SafePath.CombineFilePath(directory, filenameCopy)); copied = true; syncErrorLogFileInfo.Delete(); } } } catch (Exception ex) { Logger.Log("An error occured while checking for SYNCX.TXT files. Message: " + ex.ToString()); } return copied; } /// /// Returns the first debug snapshot directory found in Ares debug log directory that was created after last game launch and isn't empty. /// Additionally any empty snapshot directories encountered are deleted. /// /// Full path of the debug snapshot directory. If one isn't found, null is returned. private string GetNewestDebugSnapshotDirectory() { string snapshotDirectory = null; if (debugSnapshotDirectories != null) { var newDirectories = GetAllDebugSnapshotDirectories().Except(debugSnapshotDirectories); foreach (string directory in newDirectories) { if (Directory.EnumerateFileSystemEntries(directory).Any()) snapshotDirectory = directory; else { try { Directory.Delete(directory); } catch { } } } } return snapshotDirectory; } /// /// Returns list of all debug snapshot directories in Ares debug logs directory. /// /// List of all debug snapshot directories in Ares debug logs directory. Empty list if none are found or an error was encountered. private List GetAllDebugSnapshotDirectories() { var directories = new List(); try { directories.AddRange(Directory.GetDirectories(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "debug"), "snapshot-*")); } catch { } return directories; } /// /// Converts BMP screenshots to PNG and copies them from game directory to Screenshots sub-directory. /// private void ProcessScreenshots() { IEnumerable files = SafePath.GetDirectory(ProgramConstants.GamePath).EnumerateFiles("SCRN*.bmp"); DirectoryInfo screenshotsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, "Screenshots"); if (!screenshotsDirectory.Exists) { try { screenshotsDirectory.Create(); } catch (Exception ex) { Logger.Log("ProcessScreenshots: An error occured trying to create Screenshots directory. Message: " + ex.ToString()); return; } } foreach (FileInfo file in files) { try { using FileStream stream = file.OpenRead(); using var image = Image.Load(stream); FileInfo newFile = SafePath.GetFile(screenshotsDirectory.FullName, FormattableString.Invariant($"{Path.GetFileNameWithoutExtension(file.FullName)}.png")); using FileStream newFileStream = newFile.OpenWrite(); image.SaveAsPng(newFileStream); } catch (Exception ex) { Logger.Log("ProcessScreenshots: Error occured when trying to save " + Path.GetFileNameWithoutExtension(file.FullName) + ".png. Message: " + ex.ToString()); continue; } Logger.Log("ProcessScreenshots: " + Path.GetFileNameWithoutExtension(file.FullName) + ".png has been saved to Screenshots directory."); file.Delete(); } } } } ================================================ FILE: DXMainClient/DXGUI/Generic/GameLoadingWindow.cs ================================================ using ClientCore; using ClientGUI; using DTAClient.Domain; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Diagnostics; using DTAClient.DXGUI.Campaign; namespace DTAClient.DXGUI.Generic { /// /// A window for loading saved singleplayer games. /// public class GameLoadingWindow : XNAWindow { private const string SAVED_GAMES_DIRECTORY = "Saved Games"; public GameLoadingWindow(WindowManager windowManager, DiscordHandler discordHandler, CampaignTagSelector campaignTagSelector) : base(windowManager) { this.discordHandler = discordHandler; this.campaignTagSelector = campaignTagSelector; } private DiscordHandler discordHandler; private CampaignTagSelector campaignTagSelector; private XNAMultiColumnListBox lbSaveGameList; private XNAClientButton btnLaunch; private XNAClientButton btnDelete; private XNAClientButton btnCancel; private List savedGames = new List(); public override void Initialize() { Name = "GameLoadingWindow"; BackgroundTexture = AssetLoader.LoadTexture("loadmissionbg.png"); ClientRectangle = new Rectangle(0, 0, 600, 380); CenterOnParent(); lbSaveGameList = new XNAMultiColumnListBox(WindowManager); lbSaveGameList.Name = nameof(lbSaveGameList); lbSaveGameList.ClientRectangle = new Rectangle(13, 13, 574, 317); lbSaveGameList.AddColumn("SAVED GAME NAME".L10N("Client:Main:SavedGameNameColumnHeader"), 400); lbSaveGameList.AddColumn("DATE / TIME".L10N("Client:Main:SavedGameDateTimeColumnHeader"), 174); lbSaveGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbSaveGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbSaveGameList.SelectedIndexChanged += ListBox_SelectedIndexChanged; lbSaveGameList.AllowKeyboardInput = true; btnLaunch = new XNAClientButton(WindowManager); btnLaunch.Name = nameof(btnLaunch); btnLaunch.ClientRectangle = new Rectangle(125, 345, 110, 23); btnLaunch.Text = "Load".L10N("Client:Main:ButtonLoad"); btnLaunch.AllowClick = false; btnLaunch.LeftClick += BtnLaunch_LeftClick; btnDelete = new XNAClientButton(WindowManager); btnDelete.Name = nameof(btnDelete); btnDelete.ClientRectangle = new Rectangle(btnLaunch.Right + 10, btnLaunch.Y, 110, 23); btnDelete.Text = "Delete".L10N("Client:Main:ButtonDelete"); btnDelete.AllowClick = false; btnDelete.LeftClick += BtnDelete_LeftClick; btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.ClientRectangle = new Rectangle(btnDelete.Right + 10, btnLaunch.Y, 110, 23); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(lbSaveGameList); AddChild(btnLaunch); AddChild(btnDelete); AddChild(btnCancel); base.Initialize(); ListSaves(); } private void ListBox_SelectedIndexChanged(object sender, EventArgs e) { if (lbSaveGameList.SelectedIndex == -1) { btnLaunch.AllowClick = false; btnDelete.AllowClick = false; } else { btnLaunch.AllowClick = true; btnDelete.AllowClick = true; } } public void Open() { Enable(); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Disable(); } private void BtnLaunch_LeftClick(object sender, EventArgs e) { SavedGame sg = savedGames[lbSaveGameList.SelectedIndex]; Logger.Log("Loading saved game " + sg.FileName); Mission mission = campaignTagSelector.UniqueIDToMissions.GetValueOrDefault(sg.CustomMissionID, null); CustomMissionHelper.DeleteSupplementalMissionFiles(); if (mission != null) CustomMissionHelper.CopySupplementalMissionFiles(mission); FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); if (spawnerSettingsFile.Exists) spawnerSettingsFile.Delete(); IniFile spawnIni = new() { Comment = "Generated by CnCNet Client" }; IniSection spawnIniSettings = new("Settings"); spawnIniSettings.AddKey("Scenario", "spawnmap.ini"); spawnIniSettings.AddKey("SaveGameName", sg.FileName); spawnIniSettings.AddKey("LoadSaveGame", "Yes"); spawnIniSettings.AddKey("SidebarHack", ClientConfiguration.Instance.SidebarHack.ToString()); spawnIniSettings.AddKey("CustomLoadScreen", LoadingScreenController.GetLoadScreenName("g")); spawnIniSettings.AddKey("Firestorm", "No"); spawnIniSettings.AddKey("GameSpeed", UserINISettings.Instance.GameSpeed.ToString()); spawnIni.AddSection(spawnIniSettings); if (mission != null) { spawnIniSettings.AddKey("CustomMissionID", sg.CustomMissionID.ToString()); CampaignSelector.WriteMissionSectionToSpawnIni(spawnIni, mission); } spawnIni.WriteIniFile(spawnerSettingsFile.FullName); FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, "spawnmap.ini"); if (spawnMapIniFile.Exists) spawnMapIniFile.Delete(); using (var spawnMapStreamWriter = new StreamWriter(spawnMapIniFile.FullName)) { spawnMapStreamWriter.WriteLine("[Map]"); spawnMapStreamWriter.WriteLine("Size=0,0,50,50"); spawnMapStreamWriter.WriteLine("LocalSize=0,0,50,50"); spawnMapStreamWriter.WriteLine(); } discordHandler.UpdatePresence(sg.GUIName, true); Disable(); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; GameProcessLogic.StartGameProcess(WindowManager); } private void BtnDelete_LeftClick(object sender, EventArgs e) { SavedGame sg = savedGames[lbSaveGameList.SelectedIndex]; var msgBox = new XNAMessageBox(WindowManager, "Delete Confirmation".L10N("Client:Main:DeleteConfirmationTitle"), string.Format(("The following saved game will be deleted permanently:\n\n" + "Filename: {0}\n" + "Saved game name: {1}\n" + "Date and time: {2}\n\n" + "Are you sure you want to proceed?").L10N("Client:Main:DeleteConfirmationText"), sg.FileName, Renderer.GetSafeString(sg.GUIName, lbSaveGameList.FontIndex), sg.LastModified.ToString()), XNAMessageBoxButtons.YesNo); msgBox.Show(); msgBox.YesClickedAction = DeleteMsgBox_YesClicked; } private void DeleteMsgBox_YesClicked(XNAMessageBox obj) { SavedGame sg = savedGames[lbSaveGameList.SelectedIndex]; Logger.Log("Deleting saved game " + sg.FileName); SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, sg.FileName); ListSaves(); } private void GameProcessExited_Callback() { WindowManager.AddCallback(new Action(GameProcessExited), null); } protected virtual void GameProcessExited() { GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; CustomMissionHelper.DeleteSupplementalMissionFiles(); discordHandler.UpdatePresence(); } public void ListSaves() { savedGames.Clear(); lbSaveGameList.ClearItems(); lbSaveGameList.SelectedIndex = -1; DirectoryInfo savedGamesDirectoryInfo = SafePath.GetDirectory(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY); if (!savedGamesDirectoryInfo.Exists) { Logger.Log("Saved Games directory not found!"); return; } IEnumerable files = savedGamesDirectoryInfo.EnumerateFiles("*.SAV", SearchOption.TopDirectoryOnly); foreach (FileInfo file in files) { // Note: ParseSaveGame modifies savedGames ParseSaveGame(file.FullName); } savedGames = savedGames.OrderBy(sg => sg.LastModified.Ticks).ToList(); savedGames.Reverse(); foreach (SavedGame sg in savedGames) { string[] item = new string[] { Renderer.GetSafeString(sg.GUIName, lbSaveGameList.FontIndex), sg.LastModified.ToString() }; lbSaveGameList.AddItem(item, true); } } private void ParseSaveGame(string fileName) { string shortName = Path.GetFileName(fileName); SavedGame sg = new SavedGame(shortName); if (sg.ParseInfo()) savedGames.Add(sg); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/GameSessionCheckBox.cs ================================================ using System; using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.DXGUI.Multiplayer.GameLobby; using Rampastring.Tools; using Rampastring.XNAUI; namespace DTAClient.DXGUI.Generic; public enum CheckBoxMapScoringMode { /// /// The value of the check box makes no difference for scoring maps. /// Irrelevant = 0, /// /// The check box prevents map scoring when it's checked. /// DenyWhenChecked = 1, /// /// The check box prevents map scoring when it's unchecked. /// DenyWhenUnchecked = 2 } /// /// A game option check box for the game lobby or campaign. /// // TODO split the logic between descendants better and clean up public class GameSessionCheckBox : XNAClientCheckBox, IGameSessionSetting { private const int DEFAULT_SORT_ORDER = 0; public GameSessionCheckBox(WindowManager windowManager) : base (windowManager) { } public bool AllowChanges { get; set; } = true; public bool AffectsSpawnIni => !string.IsNullOrWhiteSpace(spawnIniOption); public bool AffectsMapCode => !string.IsNullOrWhiteSpace(customIniPath); public bool AllowScoring => !((mapScoringMode == CheckBoxMapScoringMode.DenyWhenChecked && Checked) || (mapScoringMode == CheckBoxMapScoringMode.DenyWhenUnchecked && !Checked)); private CheckBoxMapScoringMode mapScoringMode = CheckBoxMapScoringMode.Irrelevant; private string spawnIniOption; private string customIniPath; protected bool reversed; private string enabledSpawnIniValue = "True"; private string disabledSpawnIniValue = "False"; private bool DefaultChecked { get; set; } /// /// Whether this checkbox should be included in the GAME broadcast. /// public bool BroadcastToLobby { get; private set; } /// /// Whether the icon/text should be shown in the game list. /// public bool ShowInGameList { get; private set; } /// /// Whether the icon should be shown on the right side of the game list. /// Only applies if ShowInGameList is true. /// public bool ShowInGameListOnRight { get; private set; } /// /// Whether the icon/text should be shown in the game information panel. /// public bool ShowInGameInformationPanel { get; private set; } /// /// Whether to show only the icon (without text) in the game information panel. /// Only applies if ShowInGameInformationPanel is true. /// public bool ShowInGameInformationPanelAsIconOnly { get; private set; } /// /// Whether the icon should be shown in the game lobby control itself. /// public bool ShowIconInGameLobby { get; private set; } /// /// Whether this setting should be filterable and shown in the filters panel. /// public bool ShowInFilters { get; private set; } /// /// The texture name for the icon when setting is enabled. /// public string EnabledIcon { get; private set; } /// /// The texture name for the icon when setting is disabled. /// public string DisabledIcon { get; private set; } /// /// Sort order for displaying icons in the GameInformationPanel and GameListBox. /// Lower values appear first. /// public int SortOrder { get; private set; } = DEFAULT_SORT_ORDER; protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "SpawnIniOption": spawnIniOption = value; return; case "EnabledSpawnIniValue": enabledSpawnIniValue = value; return; case "DisabledSpawnIniValue": disabledSpawnIniValue = value; return; case "CustomIniPath": customIniPath = value; return; case "Reversed": reversed = Conversions.BooleanFromString(value, false); return; case "Checked": bool checkedValue = Conversions.BooleanFromString(value, false); DefaultChecked = Checked = checkedValue; return; case "MapScoringMode": mapScoringMode = (CheckBoxMapScoringMode)Enum.Parse(typeof(CheckBoxMapScoringMode), value); return; case "BroadcastToLobby": BroadcastToLobby = Conversions.BooleanFromString(value, false); return; case "ShowInGameList": ShowInGameList = Conversions.BooleanFromString(value, false); return; case "ShowInGameListOnRight": ShowInGameListOnRight = Conversions.BooleanFromString(value, false); return; case "ShowInGameInformationPanel": ShowInGameInformationPanel = Conversions.BooleanFromString(value, false); return; case "ShowInGameInformationPanelAsIconOnly": ShowInGameInformationPanelAsIconOnly = Conversions.BooleanFromString(value, false); return; case "ShowIconInGameLobby": ShowIconInGameLobby = Conversions.BooleanFromString(value, false); return; case "ShowInFilters": ShowInFilters = Conversions.BooleanFromString(value, false); return; case "EnabledIcon": EnabledIcon = value; return; case "DisabledIcon": DisabledIcon = value; return; case "SortOrder": SortOrder = int.Parse(value); return; } base.ParseControlINIAttribute(iniFile, key, value); } public int Value { get => Checked ? 1 : 0; // 0 = unchecked/off, 1 = checked/on set => Checked = value != 0; // 0 = unchecked/off, 1 = checked/on } public void ApplySpawnIniCode(IniFile spawnIni) { if (!AffectsSpawnIni) return; string value = disabledSpawnIniValue; if (Checked != reversed) { value = enabledSpawnIniValue; } spawnIni.SetStringValue("Settings", spawnIniOption, value); } public void ApplyMapCode(IniFile mapIni, GameMode gameMode) { if (!AffectsMapCode || Checked == reversed) return; MapCodeHelper.ApplyMapCode(mapIni, customIniPath, gameMode); } public override void OnLeftClick(InputEventArgs inputEventArgs) { // FIXME there's a discrepancy with how base XNAUI handles this // it doesn't set handled if changing the setting is not allowed inputEventArgs.Handled = true; if (!AllowChanges) return; base.OnLeftClick(inputEventArgs); } public void ResetToDefault() { if (!AllowChanges) throw new InvalidOperationException("Cannot reset to default when changes are not allowed."); Checked = DefaultChecked; } } ================================================ FILE: DXMainClient/DXGUI/Generic/GameSessionDropDown.cs ================================================ using System; using ClientCore.Extensions; using ClientCore.I18N; using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.DXGUI.Multiplayer.GameLobby; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Generic; /// /// A game option drop-down for the game lobby or campaign. /// // TODO split the logic between descendants better and clean up public class GameSessionDropDown : XNAClientDropDown, IGameSessionSetting { private const int DEFAULT_SORT_ORDER = 0; public GameSessionDropDown(WindowManager windowManager) : base(windowManager) { } public string OptionName { get; private set; } public bool AffectsSpawnIni => dataWriteMode != DropDownDataWriteMode.MAPCODE; public bool AffectsMapCode => dataWriteMode == DropDownDataWriteMode.MAPCODE; public bool AllowScoring => true; // TODO private DropDownDataWriteMode dataWriteMode = DropDownDataWriteMode.BOOLEAN; private string spawnIniOption = string.Empty; private int defaultIndex; /// /// Whether this dropdown should be included in the GAME broadcast. /// public bool BroadcastToLobby { get; private set; } /// /// Whether the icon/text should be shown in the game list. /// public bool ShowInGameList { get; private set; } /// /// Whether the icon should be shown on the right side of the game list. /// Only applies if ShowInGameList is true. /// public bool ShowInGameListOnRight { get; private set; } /// /// Whether the icon/text should be shown in the game information panel. /// public bool ShowInGameInformationPanel { get; private set; } /// /// Whether to show only the icon (without text) in the game information panel. /// Only applies if ShowInGameInformationPanel is true. /// public bool ShowInGameInformationPanelAsIconOnly { get; private set; } /// /// Whether the icon should be shown in the game lobby control itself. /// public bool ShowIconInGameLobby { get; private set; } /// /// Whether this setting should be filterable and shown in the filters panel. /// public bool ShowInFilters { get; private set; } /// /// Sort order for displaying icons in the GameInformationPanel and GameListBox. /// Lower values appear first. /// public int SortOrder { get; private set; } = DEFAULT_SORT_ORDER; protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { // shorthand for localization function static string Localize(XNAControl control, string attributeName, string defaultValue, bool notify = true) => Translation.Instance.LookUp(control, attributeName, defaultValue, notify); switch (key) { case "Items": string[] items = value.SplitWithCleanup(); string[] itemLabels = iniFile.GetStringListValue(Name, "ItemLabels", ""); string[] iconNames = iniFile.GetStringListValue(Name, "Icons", ""); for (int i = 0; i < items.Length; i++) { bool hasLabel = itemLabels.Length > i && !string.IsNullOrEmpty(itemLabels[i]); string iconName = iconNames.Length > i ? iconNames[i] : null; XNADropDownItem item = new() { Text = Localize(this, $"Item{i}", hasLabel ? itemLabels[i] : items[i]), Tag = items[i], Texture = !string.IsNullOrEmpty(iconName) ? AssetLoader.LoadTexture(iconName) : null, }; AddItem(item); } return; case "DataWriteMode": if (value.ToUpper() == "INDEX") dataWriteMode = DropDownDataWriteMode.INDEX; else if (value.ToUpper() == "BOOLEAN") dataWriteMode = DropDownDataWriteMode.BOOLEAN; else if (value.ToUpper() == "MAPCODE") dataWriteMode = DropDownDataWriteMode.MAPCODE; else dataWriteMode = DropDownDataWriteMode.STRING; return; case "SpawnIniOption": spawnIniOption = value; return; case "DefaultIndex": SelectedIndex = int.Parse(value); defaultIndex = SelectedIndex; return; case "OptionName": OptionName = Localize(this, "OptionName", value); return; case "BroadcastToLobby": BroadcastToLobby = Conversions.BooleanFromString(value, false); return; case "ShowInGameList": ShowInGameList = Conversions.BooleanFromString(value, false); return; case "ShowInGameListOnRight": ShowInGameListOnRight = Conversions.BooleanFromString(value, false); return; case "ShowInGameInformationPanel": ShowInGameInformationPanel = Conversions.BooleanFromString(value, false); return; case "ShowInGameInformationPanelAsIconOnly": ShowInGameInformationPanelAsIconOnly = Conversions.BooleanFromString(value, false); return; case "ShowIconInGameLobby": ShowIconInGameLobby = Conversions.BooleanFromString(value, false); return; case "ShowInFilters": ShowInFilters = Conversions.BooleanFromString(value, false); return; case "SortOrder": SortOrder = int.Parse(value); return; } base.ParseControlINIAttribute(iniFile, key, value); } public int Value { get => SelectedIndex; set => SelectedIndex = value; } public void ApplySpawnIniCode(IniFile spawnIni) { if (!AffectsSpawnIni || SelectedIndex < 0 || SelectedIndex >= Items.Count) return; if (String.IsNullOrEmpty(spawnIniOption)) { Logger.Log("GameLobbyDropDown.WriteSpawnIniCode: " + Name + " has no associated spawn INI option!"); return; } switch (dataWriteMode) { case DropDownDataWriteMode.BOOLEAN: spawnIni.SetBooleanValue("Settings", spawnIniOption, SelectedIndex > 0); break; case DropDownDataWriteMode.INDEX: spawnIni.SetIntValue("Settings", spawnIniOption, SelectedIndex); break; default: case DropDownDataWriteMode.STRING: spawnIni.SetStringValue("Settings", spawnIniOption, Items[SelectedIndex].Tag.ToString()); break; } } public void ApplyMapCode(IniFile mapIni, GameMode gameMode) { if (!AffectsMapCode || SelectedIndex < 0 || SelectedIndex >= Items.Count) return; string customIniPath; customIniPath = Items[SelectedIndex].Tag.ToString(); MapCodeHelper.ApplyMapCode(mapIni, customIniPath, gameMode); } public override void OnLeftClick(InputEventArgs inputEventArgs) { // FIXME there's a discrepancy with how base XNAUI handles this // it doesn't set handled if changing the setting is not allowed inputEventArgs.Handled = true; if (!AllowDropDown) return; base.OnLeftClick(inputEventArgs); } } ================================================ FILE: DXMainClient/DXGUI/Generic/LoadingScreen.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; using ClientGUI; using ClientUpdater; using DTAClient.Domain.Multiplayer; using DTAClient.Online; using Microsoft.Extensions.DependencyInjection; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System.Diagnostics; namespace DTAClient.DXGUI.Generic { public class LoadingScreen : XNAWindow { public LoadingScreen( CnCNetManager cncnetManager, WindowManager windowManager, IServiceProvider serviceProvider, MapLoader mapLoader, Random random ) : base(windowManager) { this.cncnetManager = cncnetManager; this.serviceProvider = serviceProvider; this.mapLoader = mapLoader; this.random = random; } private MapLoader mapLoader; private Random random; private bool visibleSpriteCursor; private Task updaterInitTask; private Task mapLoadTask; private readonly CnCNetManager cncnetManager; private readonly IServiceProvider serviceProvider; private List randomTextures; public override void Initialize() { ClientRectangle = new Rectangle(0, 0, 800, 600); Name = "LoadingScreen"; BackgroundTexture = AssetLoader.LoadTexture("loadingscreen.png"); base.Initialize(); CenterOnParent(); bool initUpdater = !ClientConfiguration.Instance.ModMode; if (initUpdater) { updaterInitTask = new Task(InitUpdater); updaterInitTask.Start(); } mapLoader.Initialize(); mapLoadTask = mapLoader.LoadMapsAsync(); if (Cursor.Visible) { Cursor.Visible = false; visibleSpriteCursor = true; } } protected override void GetINIAttributes(IniFile iniFile) { base.GetINIAttributes(iniFile); randomTextures = iniFile.GetStringListValue(Name, "RandomBackgroundTextures", string.Empty).ToList(); if (randomTextures.Count == 0) return; BackgroundTexture = AssetLoader.LoadTexture(randomTextures[random.Next(randomTextures.Count)]); } private void InitUpdater() { Logger.Log("Updater: Updater initialization task started."); Updater.OnLocalFileVersionsChecked += LogGameClientVersion; Updater.CheckLocalFileVersions(); Logger.Log("Updater: Updater initialization task completed."); } private void LogGameClientVersion() { Logger.Log($"Game Client Version: {ClientConfiguration.Instance.LocalGame} {Updater.GameVersion}"); Updater.OnLocalFileVersionsChecked -= LogGameClientVersion; } private void Finish() { Logger.Log("LoadingScreen: Finish waiting for updater and map loading tasks. Proceeding to main menu."); ProgramConstants.GAME_VERSION = ClientConfiguration.Instance.ModMode ? "N/A" : Updater.GameVersion; MainMenu mainMenu = serviceProvider.GetService(); WindowManager.AddAndInitializeControl(mainMenu); mainMenu.PostInit(); if (UserINISettings.Instance.AutomaticCnCNetLogin && NameValidator.IsNameValid(ProgramConstants.PLAYERNAME, out _) == NameValidationError.None) { cncnetManager.Connect(); } if (!UserINISettings.Instance.PrivacyPolicyAccepted) { WindowManager.AddAndInitializeControl(new PrivacyNotification(WindowManager)); } WindowManager.RemoveControl(this); Cursor.Visible = visibleSpriteCursor; } private TimeSpan Update_LastLogTime = TimeSpan.Zero; public override void Update(GameTime gameTime) { base.Update(gameTime); bool updaterDone = updaterInitTask == null || updaterInitTask.Status == TaskStatus.RanToCompletion; bool mapLoadDone = mapLoadTask.Status == TaskStatus.RanToCompletion; if (updaterDone && mapLoadDone) { Finish(); return; } var timeSinceLastLog = gameTime.TotalGameTime.Subtract(Update_LastLogTime); if (timeSinceLastLog > TimeSpan.FromSeconds(5)) { Update_LastLogTime = gameTime.TotalGameTime; string logMessage; if (!updaterDone && !mapLoadDone) logMessage = "LoadingScreen: Waiting for updater initialization and loading maps..."; else if (!updaterDone) logMessage = "LoadingScreen: Waiting for updater initialization..."; else if (!mapLoadDone) logMessage = "LoadingScreen: Waiting for loading maps..."; else throw new Exception("Assert failed. No pending tasks. This should not happen."); Debug.WriteLine(logMessage); Logger.Log(logMessage); } } } } ================================================ FILE: DXMainClient/DXGUI/Generic/MainMenu.cs ================================================ using ClientCore; using ClientCore.Enums; using ClientCore.I18N; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; using DTAClient.Online; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using ClientUpdater; using DTAClient.Domain.Multiplayer; using DTAClient.DXGUI.Campaign; namespace DTAClient.DXGUI.Generic { /// /// The main menu of the client. /// class MainMenu : XNAWindow, ISwitchable { private const float MEDIA_PLAYER_VOLUME_FADE_STEP = 0.01f; private const float MEDIA_PLAYER_VOLUME_EXIT_FADE_STEP = 0.025f; private const double UPDATE_RE_CHECK_THRESHOLD = 30.0; /// /// Creates a new instance of the main menu. /// public MainMenu( WindowManager windowManager, SkirmishLobby skirmishLobby, LANLobby lanLobby, TopBar topBar, OptionsWindow optionsWindow, CnCNetLobby cncnetLobby, CnCNetManager connectionManager, DiscordHandler discordHandler, CnCNetGameLoadingLobby cnCNetGameLoadingLobby, CnCNetGameLobby cnCNetGameLobby, PrivateMessagingPanel privateMessagingPanel, PrivateMessagingWindow privateMessagingWindow, GameInProgressWindow gameInProgressWindow, MapLoader mapLoader, CampaignTagSelector campaignTagSelector, GameLoadingWindow gameLoadingWindow, StatisticsWindow statisticsWindow, UpdateQueryWindow updateQueryWindow, ManualUpdateQueryWindow manualUpdateQueryWindow, UpdateWindow updateWindow, ExtrasWindow extrasWindow, DirectDrawWrapperManager directDrawWrapperManager ) : base(windowManager) { this.lanLobby = lanLobby; this.topBar = topBar; this.connectionManager = connectionManager; this.optionsWindow = optionsWindow; this.cncnetLobby = cncnetLobby; this.discordHandler = discordHandler; this.skirmishLobby = skirmishLobby; this.cnCNetGameLoadingLobby = cnCNetGameLoadingLobby; this.cnCNetGameLobby = cnCNetGameLobby; this.privateMessagingPanel = privateMessagingPanel; this.privateMessagingWindow = privateMessagingWindow; this.gameInProgressWindow = gameInProgressWindow; this.mapLoader = mapLoader; this.campaignTagSelector = campaignTagSelector; this.gameLoadingWindow = gameLoadingWindow; this.statisticsWindow = statisticsWindow; this.updateQueryWindow = updateQueryWindow; this.manualUpdateQueryWindow = manualUpdateQueryWindow; this.updateWindow = updateWindow; this.extrasWindow = extrasWindow; this.directDrawWrapperManager = directDrawWrapperManager; this.cncnetLobby.UpdateCheck += CncnetLobby_UpdateCheck; isMediaPlayerAvailable = IsMediaPlayerAvailable(); } private XNALabel lblCnCNetPlayerCount; private XNALinkLabel lblUpdateStatus; private XNALinkLabel lblVersion; private CnCNetLobby cncnetLobby; private SkirmishLobby skirmishLobby; private LANLobby lanLobby; private CnCNetManager connectionManager; private OptionsWindow optionsWindow; private DiscordHandler discordHandler; private TopBar topBar; private readonly CnCNetGameLoadingLobby cnCNetGameLoadingLobby; private readonly CnCNetGameLobby cnCNetGameLobby; private readonly PrivateMessagingPanel privateMessagingPanel; private readonly PrivateMessagingWindow privateMessagingWindow; private readonly GameInProgressWindow gameInProgressWindow; private readonly MapLoader mapLoader; private readonly CampaignTagSelector campaignTagSelector; private readonly GameLoadingWindow gameLoadingWindow; private readonly StatisticsWindow statisticsWindow; private readonly UpdateQueryWindow updateQueryWindow; private readonly ManualUpdateQueryWindow manualUpdateQueryWindow; private readonly UpdateWindow updateWindow; private readonly ExtrasWindow extrasWindow; private readonly DirectDrawWrapperManager directDrawWrapperManager; private XNAMessageBox firstRunMessageBox; private bool _updateInProgress; private bool UpdateInProgress { get { return _updateInProgress; } set { _updateInProgress = value; topBar.SetSwitchButtonsClickable(!_updateInProgress); topBar.SetOptionsButtonClickable(!_updateInProgress); SetButtonHotkeys(!_updateInProgress); } } private bool customComponentDialogQueued = false; private DateTime lastUpdateCheckTime; private Song themeSong; private static readonly object locker = new object(); private bool isMusicFading = false; private readonly bool isMediaPlayerAvailable; private CancellationTokenSource cncnetPlayerCountCancellationSource; // Main Menu Buttons private XNAClientButton btnNewCampaign; private XNAClientButton btnLoadGame; private XNAClientButton btnSkirmish; private XNAClientButton btnCnCNet; private XNAClientButton btnLan; private XNAClientButton btnOptions; private XNAClientButton btnMapEditor; private XNAClientButton btnStatistics; private XNAClientButton btnCredits; private XNAClientButton btnExtras; /// /// Initializes the main menu's controls. /// public override void Initialize() { topBar.SetSecondarySwitch(cncnetLobby); GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; Name = nameof(MainMenu); BackgroundTexture = AssetLoader.LoadTexture("MainMenu/mainmenubg.png"); ClientRectangle = new Rectangle(0, 0, BackgroundTexture.Width, BackgroundTexture.Height); WindowManager.CenterControlOnScreen(this); btnNewCampaign = new XNAClientButton(WindowManager); btnNewCampaign.Name = nameof(btnNewCampaign); btnNewCampaign.IdleTexture = AssetLoader.LoadTexture("MainMenu/campaign.png"); btnNewCampaign.HoverTexture = AssetLoader.LoadTexture("MainMenu/campaign_c.png"); btnNewCampaign.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnNewCampaign.LeftClick += BtnNewCampaign_LeftClick; btnLoadGame = new XNAClientButton(WindowManager); btnLoadGame.Name = nameof(btnLoadGame); btnLoadGame.IdleTexture = AssetLoader.LoadTexture("MainMenu/loadmission.png"); btnLoadGame.HoverTexture = AssetLoader.LoadTexture("MainMenu/loadmission_c.png"); btnLoadGame.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnLoadGame.LeftClick += BtnLoadGame_LeftClick; btnSkirmish = new XNAClientButton(WindowManager); btnSkirmish.Name = nameof(btnSkirmish); btnSkirmish.IdleTexture = AssetLoader.LoadTexture("MainMenu/skirmish.png"); btnSkirmish.HoverTexture = AssetLoader.LoadTexture("MainMenu/skirmish_c.png"); btnSkirmish.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnSkirmish.LeftClick += BtnSkirmish_LeftClick; btnCnCNet = new XNAClientButton(WindowManager); btnCnCNet.Name = nameof(btnCnCNet); btnCnCNet.IdleTexture = AssetLoader.LoadTexture("MainMenu/cncnet.png"); btnCnCNet.HoverTexture = AssetLoader.LoadTexture("MainMenu/cncnet_c.png"); btnCnCNet.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnCnCNet.LeftClick += BtnCnCNet_LeftClick; btnLan = new XNAClientButton(WindowManager); btnLan.Name = nameof(btnLan); btnLan.IdleTexture = AssetLoader.LoadTexture("MainMenu/lan.png"); btnLan.HoverTexture = AssetLoader.LoadTexture("MainMenu/lan_c.png"); btnLan.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnLan.LeftClick += BtnLan_LeftClick; btnOptions = new XNAClientButton(WindowManager); btnOptions.Name = nameof(btnOptions); btnOptions.IdleTexture = AssetLoader.LoadTexture("MainMenu/options.png"); btnOptions.HoverTexture = AssetLoader.LoadTexture("MainMenu/options_c.png"); btnOptions.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnOptions.LeftClick += BtnOptions_LeftClick; btnMapEditor = new XNAClientButton(WindowManager); btnMapEditor.Name = nameof(btnMapEditor); btnMapEditor.IdleTexture = AssetLoader.LoadTexture("MainMenu/mapeditor.png"); btnMapEditor.HoverTexture = AssetLoader.LoadTexture("MainMenu/mapeditor_c.png"); btnMapEditor.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnMapEditor.LeftClick += BtnMapEditor_LeftClick; btnStatistics = new XNAClientButton(WindowManager); btnStatistics.Name = nameof(btnStatistics); btnStatistics.IdleTexture = AssetLoader.LoadTexture("MainMenu/statistics.png"); btnStatistics.HoverTexture = AssetLoader.LoadTexture("MainMenu/statistics_c.png"); btnStatistics.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnStatistics.LeftClick += BtnStatistics_LeftClick; btnCredits = new XNAClientButton(WindowManager); btnCredits.Name = nameof(btnCredits); btnCredits.IdleTexture = AssetLoader.LoadTexture("MainMenu/credits.png"); btnCredits.HoverTexture = AssetLoader.LoadTexture("MainMenu/credits_c.png"); btnCredits.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnCredits.LeftClick += BtnCredits_LeftClick; btnExtras = new XNAClientButton(WindowManager); btnExtras.Name = nameof(btnExtras); btnExtras.IdleTexture = AssetLoader.LoadTexture("MainMenu/extras.png"); btnExtras.HoverTexture = AssetLoader.LoadTexture("MainMenu/extras_c.png"); btnExtras.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnExtras.LeftClick += BtnExtras_LeftClick; var btnExit = new XNAClientButton(WindowManager); btnExit.Name = nameof(btnExit); btnExit.IdleTexture = AssetLoader.LoadTexture("MainMenu/exitgame.png"); btnExit.HoverTexture = AssetLoader.LoadTexture("MainMenu/exitgame_c.png"); btnExit.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); btnExit.LeftClick += BtnExit_LeftClick; XNALabel lblCnCNetStatus = new XNALabel(WindowManager); lblCnCNetStatus.Name = nameof(lblCnCNetStatus); lblCnCNetStatus.Text = "DTA players on CnCNet:".L10N("Client:Main:CnCNetOnlinePlayersCountText"); lblCnCNetStatus.ClientRectangle = new Rectangle(12, 9, 0, 0); lblCnCNetPlayerCount = new XNALabel(WindowManager); lblCnCNetPlayerCount.Name = nameof(lblCnCNetPlayerCount); lblCnCNetPlayerCount.Text = "-"; lblVersion = new XNALinkLabel(WindowManager); lblVersion.Name = nameof(lblVersion); lblVersion.LeftClick += LblVersion_LeftClick; lblUpdateStatus = new XNALinkLabel(WindowManager); lblUpdateStatus.Name = nameof(lblUpdateStatus); lblUpdateStatus.LeftClick += LblUpdateStatus_LeftClick; lblUpdateStatus.ClientRectangle = new Rectangle(0, 0, UIDesignConstants.BUTTON_WIDTH_160, 20); AddChild(btnNewCampaign); AddChild(btnLoadGame); AddChild(btnSkirmish); AddChild(btnCnCNet); AddChild(btnLan); AddChild(btnOptions); AddChild(btnMapEditor); AddChild(btnStatistics); AddChild(btnCredits); AddChild(btnExtras); AddChild(btnExit); AddChild(lblCnCNetStatus); AddChild(lblCnCNetPlayerCount); if (!ClientConfiguration.Instance.ModMode) { // ModMode disables version tracking and the updater if it's enabled AddChild(lblVersion); AddChild(lblUpdateStatus); Updater.FileIdentifiersUpdated += Updater_FileIdentifiersUpdated; Updater.OnCustomComponentsOutdated += Updater_OnCustomComponentsOutdated; } base.Initialize(); // Read control attributes from INI lblVersion.Text = Updater.GameVersion; updateQueryWindow.UpdateDeclined += UpdateQueryWindow_UpdateDeclined; updateQueryWindow.UpdateAccepted += UpdateQueryWindow_UpdateAccepted; manualUpdateQueryWindow.Closed += ManualUpdateQueryWindow_Closed; updateWindow.UpdateCompleted += UpdateWindow_UpdateCompleted; updateWindow.UpdateCancelled += UpdateWindow_UpdateCancelled; updateWindow.UpdateFailed += UpdateWindow_UpdateFailed; ClientRectangle = new Rectangle((WindowManager.RenderResolutionX - Width) / 2, (WindowManager.RenderResolutionY - Height) / 2, Width, Height); CnCNetPlayerCountTask.CnCNetGameCountUpdated += CnCNetInfoController_CnCNetGameCountUpdated; cncnetPlayerCountCancellationSource = new CancellationTokenSource(); CnCNetPlayerCountTask.InitializeService(cncnetPlayerCountCancellationSource); WindowManager.GameClosing += WindowManager_GameClosing; skirmishLobby.Exited += SkirmishLobby_Exited; lanLobby.Exited += LanLobby_Exited; optionsWindow.EnabledChanged += OptionsWindow_EnabledChanged; optionsWindow.OnForceUpdate += (s, e) => ForceUpdate(); GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted; GameProcessLogic.GameProcessStarting += SharedUILogic_GameProcessStarting; UserINISettings.Instance.SettingsSaved += SettingsSaved; Updater.Restart += Updater_Restart; SetButtonHotkeys(true); } private void SetButtonHotkeys(bool enableHotkeys) { if (!Initialized) return; if (enableHotkeys) { btnNewCampaign.HotKey = Keys.C; btnLoadGame.HotKey = Keys.L; btnSkirmish.HotKey = Keys.S; btnCnCNet.HotKey = Keys.M; btnLan.HotKey = Keys.N; btnOptions.HotKey = Keys.O; btnMapEditor.HotKey = Keys.E; btnStatistics.HotKey = Keys.T; btnCredits.HotKey = Keys.R; btnExtras.HotKey = Keys.X; } else { btnNewCampaign.HotKey = Keys.None; btnLoadGame.HotKey = Keys.None; btnSkirmish.HotKey = Keys.None; btnCnCNet.HotKey = Keys.None; btnLan.HotKey = Keys.None; btnOptions.HotKey = Keys.None; btnMapEditor.HotKey = Keys.None; btnStatistics.HotKey = Keys.None; btnCredits.HotKey = Keys.None; btnExtras.HotKey = Keys.None; } } private void OptionsWindow_EnabledChanged(object sender, EventArgs e) { if (!optionsWindow.Enabled) { if (customComponentDialogQueued) Updater_OnCustomComponentsOutdated(); } } /// /// Refreshes settings. Called when the game process is starting. /// private void SharedUILogic_GameProcessStarting() { UserINISettings.Instance.ReloadSettings(); try { optionsWindow.RefreshSettings(); } catch (Exception ex) { Logger.Log("Refreshing settings failed! Exception message: " + ex.ToString()); // We don't want to show the dialog when starting a game //XNAMessageBox.Show(WindowManager, "Saving settings failed", // "Saving settings failed! Error message: " + ex.Message); } } private void Updater_Restart(object sender, EventArgs e) => WindowManager.AddCallback(new Action(ExitClient), null); /// /// Applies configuration changes (music playback and volume) /// when settings are saved. /// private void SettingsSaved(object sender, EventArgs e) { if (isMediaPlayerAvailable) { if (MediaPlayer.State == MediaState.Playing) { if (!UserINISettings.Instance.PlayMainMenuMusic) isMusicFading = true; } else if (topBar.GetTopMostPrimarySwitchable() == this && topBar.LastSwitchType == SwitchType.PRIMARY) { PlayMusic(); } } if (!connectionManager.IsConnected) ProgramConstants.PLAYERNAME = UserINISettings.Instance.PlayerName; if (UserINISettings.Instance.DiscordIntegration && !ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) discordHandler.Connect(); else discordHandler.Disconnect(); } /// /// Checks files which are required for the mod to function /// but not distributed with the mod (usually base game files /// for YR mods which can't be standalone). /// private void CheckRequiredFiles() { List absentFiles = ClientConfiguration.Instance.RequiredFiles.ToList() .FindAll(f => !string.IsNullOrWhiteSpace(f) && !SafePath.GetFile(ProgramConstants.GamePath, f).Exists); if (absentFiles.Count > 0) { string description = string.Empty; if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) { description = ("You are missing Yuri's Revenge files that are required\n" + "to play this mod! Yuri's Revenge mods are not standalone,\n" + "so you need a copy of following Yuri's Revenge (v.1.001)\n" + "files placed in the mod folder to play the mod:").L10N("Client:Main:MissingFilesText1Ares"); } else { description = "The following required files are missing:".L10N("Client:Main:MissingFilesText1NonAres"); } description += Environment.NewLine + Environment.NewLine + String.Join(Environment.NewLine, absentFiles) + Environment.NewLine + Environment.NewLine + "You won't be able to play without those files.".L10N("Client:Main:MissingFilesText2"); XNAMessageBox.Show(WindowManager, "Missing Files".L10N("Client:Main:MissingFilesTitle"), description); } } private void CheckForbiddenFiles() { List presentFiles = ClientConfiguration.Instance.ForbiddenFiles.ToList() .FindAll(f => !string.IsNullOrWhiteSpace(f) && SafePath.GetFile(ProgramConstants.GamePath, f).Exists); if (presentFiles.Count > 0) { string description; if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) { description = ("You have installed the mod on top of a Tiberian Sun\n" + "copy! This mod is standalone, therefore you have to\n" + "install it in an empty folder. Otherwise the mod won't\n" + "function correctly.\n\n" + "Please reinstall the mod into an empty folder to play.").L10N("Client:Main:InterferingFilesDetectedTextTS"); } else { description = "The following interfering files are present:".L10N("Client:Main:InterferingFilesDetectedTextNonTS1") + Environment.NewLine + Environment.NewLine + String.Join(Environment.NewLine, presentFiles) + Environment.NewLine + Environment.NewLine + "The mod won't work correctly without those files removed.".L10N("Client:Main:InterferingFilesDetectedTextNonTS2"); } XNAMessageBox.Show(WindowManager, "Interfering Files Detected".L10N("Client:Main:InterferingFilesDetectedTitle"), description); } } /// /// Checks whether the client is running for the first time. /// If it is, displays a dialog asking the user if they'd like /// to configure settings. /// private void CheckIfFirstRun() { if (UserINISettings.Instance.IsFirstRun) { UserINISettings.Instance.IsFirstRun.Value = false; UserINISettings.Instance.SaveSettings(); firstRunMessageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Initial Installation".L10N("Client:Main:InitialInstallationTitle"), string.Format(("You have just installed {0}.\n" + "It's highly recommended that you configure your settings before playing.\n" + "Do you want to configure them now?").L10N("Client:Main:InitialInstallationText"), ClientConfiguration.Instance.LocalGame)); firstRunMessageBox.YesClickedAction = FirstRunMessageBox_YesClicked; firstRunMessageBox.NoClickedAction = FirstRunMessageBox_NoClicked; } optionsWindow.PostInit(); } private void CheckAndApplyTranslationGameFiles(bool skipVersionCheck = false) { // In ModMode there is no updater, so always apply translation game files. // Otherwise, skip if already applied for the current game version. if (!skipVersionCheck && !ClientConfiguration.Instance.ModMode && UserINISettings.Instance.TranslationGameFilesVersion.Value == Updater.GameVersion) return; try { Translation.Instance.ApplyTranslationGameFiles(); UserINISettings.Instance.TranslationGameFilesVersion.Value = Updater.GameVersion; UserINISettings.Instance.SaveSettings(); } catch (Exception ex) { Logger.Log("Failed to apply translation game files. " + ex.ToString()); XNAMessageBox.Show(WindowManager, "Applying Translation Files Failed".L10N("Client:Main:ApplyTranslationFilesFailTitle"), "Applying translation files failed! Error message:".L10N("Client:Main:ApplyTranslationFilesFailText") + " " + ex.Message); } } private void FirstRunMessageBox_NoClicked(XNAMessageBox messageBox) { if (customComponentDialogQueued) Updater_OnCustomComponentsOutdated(); } private void FirstRunMessageBox_YesClicked(XNAMessageBox messageBox) => optionsWindow.Open(); private void SharedUILogic_GameProcessStarted() => MusicOff(); private void WindowManager_GameClosing(object sender, EventArgs e) => Clean(); private void SkirmishLobby_Exited(object sender, EventArgs e) { if (UserINISettings.Instance.StopMusicOnMenu) PlayMusic(); } private void LanLobby_Exited(object sender, EventArgs e) { topBar.SetLanMode(false); if (UserINISettings.Instance.AutomaticCnCNetLogin) connectionManager.Connect(); if (UserINISettings.Instance.StopMusicOnMenu) PlayMusic(); } private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCountEventArgs e) { lock (locker) { if (e.PlayerCount == -1) lblCnCNetPlayerCount.Text = "N/A".L10N("Client:Main:N/A"); else lblCnCNetPlayerCount.Text = e.PlayerCount.ToString(); } } /// /// Attemps to "clean" the client session in a nice way if the user closes the game. /// private void Clean() { Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated; if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel(); topBar.Clean(); if (UpdateInProgress) Updater.StopUpdate(); if (connectionManager.IsConnected) connectionManager.Disconnect(); } /// /// Starts playing music, initiates an update check if automatic updates /// are enabled and checks whether the client is run for the first time. /// Called after all internal client UI logic has been initialized. /// public void PostInit() { Logger.Log("Main menu post-initialization started."); foreach (XNAControl control in new XNAControl[] { statisticsWindow, // Note: StatisticsWindow must be initialized before any lobbies that extends GameLobbyBase. This is because StatisticsManager is accessed when initializing GameLobbyBase. skirmishLobby, cnCNetGameLoadingLobby, cnCNetGameLobby, cncnetLobby, lanLobby, campaignTagSelector, gameLoadingWindow, updateQueryWindow, manualUpdateQueryWindow, updateWindow, extrasWindow, }) DarkeningPanel.AddAndInitializeWithControl(WindowManager, control); optionsWindow.SetTopBar(topBar); DarkeningPanel.AddAndInitializeWithControl(WindowManager, optionsWindow); WindowManager.AddAndInitializeControl(privateMessagingPanel); privateMessagingPanel.AddChild(privateMessagingWindow); topBar.SetTertiarySwitch(privateMessagingWindow); topBar.SetOptionsWindow(optionsWindow); WindowManager.AddAndInitializeControl(gameInProgressWindow); foreach (XNAControl control in new XNAControl[] { skirmishLobby, cnCNetGameLoadingLobby, cnCNetGameLobby, cncnetLobby, lanLobby, privateMessagingWindow, optionsWindow, campaignTagSelector, gameLoadingWindow, statisticsWindow, updateQueryWindow, manualUpdateQueryWindow, updateWindow, extrasWindow, }) control.Disable(); WindowManager.AddAndInitializeControl(topBar); topBar.AddPrimarySwitchable(this); RevertSwitchMainMenuMusicFormat(); LoadThemeSong(); PlayMusic(); if (!ClientConfiguration.Instance.ModMode) { if (Updater.UpdateMirrors.Count < 1) { lblUpdateStatus.Text = "No update download mirrors available.".L10N("Client:Main:NoUpdateMirrorsAvailable"); lblUpdateStatus.DrawUnderline = false; } else if (UserINISettings.Instance.CheckForUpdates) { CheckForUpdates(); } else { lblUpdateStatus.Text = "Click to check for updates.".L10N("Client:Main:ClickToCheckUpdate"); } } CheckRequiredFiles(); CheckForbiddenFiles(); CheckIfFirstRun(); CheckAndApplyTranslationGameFiles(); Logger.Log("Main menu initialization complete."); Logger.Log(FormattableString.Invariant($"Startup complete. Client is ready. Total startup time: {PreStartup.StartupElapsed.TotalSeconds:F3} s.")); MainClientConstants.DisplayErrorAction = (title, error, exit) => { new XNAMessageBox(WindowManager, title, error, XNAMessageBoxButtons.OK) { OKClickedAction = _ => { if (exit) Environment.Exit(1); }, }.Show(); }; #if ISWINDOWS if (!directDrawWrapperManager.SelectedRenderer.IsDummy) DirectDrawCompatibilityChecker.CheckAndPromptFix(WindowManager); #endif } private void LoadThemeSong() { #if XNA themeSong = AssetLoader.LoadSong(ClientConfiguration.Instance.MainMenuMusicName); #else #if GL string songExtension = "ogg"; #elif DX string songExtension = "wma"; #endif FileInfo mainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, FormattableString.Invariant($"{ClientConfiguration.Instance.MainMenuMusicName}.{songExtension}")); if (!mainMenuMusicFile.Exists) return; try { themeSong = Song.FromUri(ClientConfiguration.Instance.MainMenuMusicName, new Uri(mainMenuMusicFile.FullName)); } catch (Exception ex) { Logger.Log($"Error loading the theme song. Fallback to the legacy method. Have you installed 'Media Feature Pack for Windows 10/11 N'? Exception: {ex.ToString()}"); themeSong = AssetLoader.LoadSong(ClientConfiguration.Instance.MainMenuMusicName); } #endif } private void RevertSwitchMainMenuMusicFormat() { FileInfo wmaBackupMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, FormattableString.Invariant($"{ClientConfiguration.Instance.MainMenuMusicName}.bak")); if (wmaBackupMainMenuMusicFile.Exists) { FileInfo wmaMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, FormattableString.Invariant($"{ClientConfiguration.Instance.MainMenuMusicName}.wma")); if (wmaMainMenuMusicFile.Exists) wmaMainMenuMusicFile.Delete(); wmaBackupMainMenuMusicFile.MoveTo(wmaMainMenuMusicFile.FullName); } } #region Updating / versioning system private void UpdateWindow_UpdateFailed(object sender, UpdateFailureEventArgs e) { updateWindow.Disable(); lblUpdateStatus.Text = "Updating failed! Click to retry.".L10N("Client:Main:UpdateFailedClickToRetry"); lblUpdateStatus.DrawUnderline = true; lblUpdateStatus.Enabled = true; UpdateInProgress = false; // TODO Enable a dummy Window from DarkeningPanel -- seems not needed. This message box works well. XNAMessageBox msgBox = new XNAMessageBox(WindowManager, "Update failed".L10N("Client:Main:UpdateFailedTitle"), string.Format(("An error occured while updating. Returned error was: {0}\n\nIf you are connected to the Internet and your firewall isn't blocking\n{1}, and the issue is reproducible, contact us at\n{2} for support.").L10N("Client:Main:UpdateFailedText"), e.Reason, Path.GetFileName(ProgramConstants.StartupExecutable), MainClientConstants.SUPPORT_URL_SHORT), XNAMessageBoxButtons.OK); msgBox.OKClickedAction = (XNAMessageBox messageBox) => { // TODO Disable the dummy Window from DarkeningPanel -- seems not needed. This message box works well. }; msgBox.Show(); } private void UpdateWindow_UpdateCancelled(object sender, EventArgs e) { updateWindow.Disable(); lblUpdateStatus.Text = "The update was cancelled. Click to retry.".L10N("Client:Main:UpdateCancelledClickToRetry"); lblUpdateStatus.DrawUnderline = true; lblUpdateStatus.Enabled = true; UpdateInProgress = false; } private void UpdateWindow_UpdateCompleted(object sender, EventArgs e) { updateWindow.Disable(); lblUpdateStatus.Text = string.Format("{0} was succesfully updated to v.{1}".L10N("Client:Main:UpdateSuccess"), MainClientConstants.GAME_NAME_SHORT, Updater.GameVersion); lblVersion.Text = Updater.GameVersion; UpdateInProgress = false; lblUpdateStatus.Enabled = true; lblUpdateStatus.DrawUnderline = false; // The update completed without requiring a client restart, so apply // translation game files immediately for the new game version. // (If a restart were required, Updater.Restart fires and the client // exits; the next startup naturally detects the version change.) CheckAndApplyTranslationGameFiles(skipVersionCheck: true); } private void LblUpdateStatus_LeftClick(object sender, EventArgs e) { Logger.Log(Updater.VersionState.ToString()); if (Updater.VersionState == VersionState.OUTDATED || Updater.VersionState == VersionState.MISMATCHED || Updater.VersionState == VersionState.UNKNOWN || Updater.VersionState == VersionState.UPTODATE) { CheckForUpdates(); } } private void LblVersion_LeftClick(object sender, EventArgs e) { ProcessLauncher.StartShellProcess(ClientConfiguration.Instance.ChangelogURL); } private void ForceUpdate() { UpdateInProgress = true; optionsWindow.Disable(); updateWindow.ForceUpdate(); updateWindow.Enable(); lblUpdateStatus.Text = "Force updating...".L10N("Client:Main:ForceUpdating"); } /// /// Starts a check for updates. /// private void CheckForUpdates() { if (Updater.UpdateMirrors.Count < 1) return; Updater.CheckForUpdates(); lblUpdateStatus.Enabled = false; lblUpdateStatus.Text = "Checking for updates..." .L10N("Client:Main:CheckingForUpdates"); lastUpdateCheckTime = DateTime.Now; } private void Updater_FileIdentifiersUpdated() => WindowManager.AddCallback(new Action(HandleFileIdentifierUpdate), null); /// /// Used for displaying the result of an update check in the UI. /// private void HandleFileIdentifierUpdate() { if (UpdateInProgress) { return; } if (Updater.VersionState == VersionState.UPTODATE) { lblUpdateStatus.Text = string.Format("{0} is up to date.".L10N("Client:Main:GameUpToDate"), MainClientConstants.GAME_NAME_SHORT); lblUpdateStatus.Enabled = true; lblUpdateStatus.DrawUnderline = false; } else if (Updater.VersionState == VersionState.OUTDATED && Updater.ManualUpdateRequired) { lblUpdateStatus.Text = "An update is available. Manual download & installation required.".L10N("Client:Main:UpdateAvailableManualDownloadRequired"); lblUpdateStatus.Enabled = true; lblUpdateStatus.DrawUnderline = false; manualUpdateQueryWindow.SetInfo(Updater.ServerGameVersion, Updater.ManualDownloadURL); if (!string.IsNullOrEmpty(Updater.ManualDownloadURL)) manualUpdateQueryWindow.Enable(); } else if (Updater.VersionState == VersionState.OUTDATED) { lblUpdateStatus.Text = "An update is available.".L10N("Client:Main:UpdateAvailable"); updateQueryWindow.SetInfo(Updater.ServerGameVersion, Updater.UpdateSizeInKb); updateQueryWindow.Enable(); } else if (Updater.VersionState == VersionState.UNKNOWN) { lblUpdateStatus.Text = "Checking for updates failed! Click to retry.".L10N("Client:Main:CheckUpdateFailedClickToRetry"); lblUpdateStatus.Enabled = true; lblUpdateStatus.DrawUnderline = true; } } /// /// Asks the user if they'd like to update their custom components. /// Handles an event raised by the updater when it has detected /// that the custom components are out of date. /// private void Updater_OnCustomComponentsOutdated() { if (updateQueryWindow.Visible) return; if (UpdateInProgress) return; if ((firstRunMessageBox != null && firstRunMessageBox.Visible) || optionsWindow.Enabled) { // If the custom components are out of date on the first run // or the options window is already open, don't show the dialog customComponentDialogQueued = true; return; } customComponentDialogQueued = false; XNAMessageBox ccMsgBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Custom Component Updates Available".L10N("Client:Main:CustomUpdateAvailableTitle"), ("Updates for custom components are available. Do you want to open\nthe Options menu where you can update the custom components?").L10N("Client:Main:CustomUpdateAvailableText")); ccMsgBox.YesClickedAction = CCMsgBox_YesClicked; } private void CCMsgBox_YesClicked(XNAMessageBox messageBox) { optionsWindow.Open(); optionsWindow.SwitchToCustomComponentsPanel(); } /// /// Called when the user has declined an update. /// private void UpdateQueryWindow_UpdateDeclined(object sender, EventArgs e) { updateQueryWindow.Disable(); lblUpdateStatus.Text = "An update is available, click to install.".L10N("Client:Main:UpdateAvailableClickToInstall"); lblUpdateStatus.Enabled = true; lblUpdateStatus.DrawUnderline = true; } /// /// Called when the user has accepted an update. /// private void UpdateQueryWindow_UpdateAccepted(object sender, EventArgs e) { updateQueryWindow.Disable(); updateWindow.SetData(Updater.ServerGameVersion); updateWindow.Enable(); lblUpdateStatus.Text = "Updating...".L10N("Client:Main:Updating"); UpdateInProgress = true; Updater.StartUpdate(); } private void ManualUpdateQueryWindow_Closed(object sender, EventArgs e) => manualUpdateQueryWindow.Disable(); #endregion private void BtnOptions_LeftClick(object sender, EventArgs e) => optionsWindow.Open(); private void BtnNewCampaign_LeftClick(object sender, EventArgs e) => campaignTagSelector.Open(); private void BtnLoadGame_LeftClick(object sender, EventArgs e) => gameLoadingWindow.Enable(); private void BtnLan_LeftClick(object sender, EventArgs e) { lanLobby.Open(); if (UserINISettings.Instance.StopMusicOnMenu) MusicOff(); if (connectionManager.IsConnected) connectionManager.Disconnect(); topBar.SetLanMode(true); } private void BtnCnCNet_LeftClick(object sender, EventArgs e) => topBar.SwitchToSecondary(); private void BtnSkirmish_LeftClick(object sender, EventArgs e) { skirmishLobby.Open(); if (UserINISettings.Instance.StopMusicOnMenu) MusicOff(); } private void BtnMapEditor_LeftClick(object sender, EventArgs e) => LaunchMapEditor(); private void BtnStatistics_LeftClick(object sender, EventArgs e) => statisticsWindow.Enable(); private void BtnCredits_LeftClick(object sender, EventArgs e) { ProcessLauncher.StartShellProcess(ClientConfiguration.Instance.CreditsURL); } private void BtnExtras_LeftClick(object sender, EventArgs e) => extrasWindow.Enable(); private void BtnExit_LeftClick(object sender, EventArgs e) { #if WINFORMS WindowManager.HideWindow(); #endif FadeMusicExit(); } private void SharedUILogic_GameProcessExited() => AddCallback(new Action(HandleGameProcessExited), null); private void HandleGameProcessExited() { gameLoadingWindow.ListSaves(); gameLoadingWindow.Disable(); gameInProgressWindow.Disable(); // If music is disabled on menus, check if the main menu is the top-most // window of the top bar and only play music if it is // LAN has the top bar disabled, so to detect the LAN game lobby // we'll check whether the top bar is enabled if (!UserINISettings.Instance.StopMusicOnMenu || (topBar.Enabled && topBar.LastSwitchType == SwitchType.PRIMARY && topBar.GetTopMostPrimarySwitchable() == this)) PlayMusic(); } /// /// Switches to the main menu and performs a check for updates. /// private void CncnetLobby_UpdateCheck(object sender, EventArgs e) { CheckForUpdates(); topBar.SwitchToPrimary(); } public override void Update(GameTime gameTime) { if (isMusicFading) FadeMusic(gameTime); base.Update(gameTime); } public override void Draw(GameTime gameTime) { lock (locker) { base.Draw(gameTime); } } /// /// Attempts to start playing the menu music. /// private void PlayMusic() { if (!isMediaPlayerAvailable) return; // SharpDX fails at music playback on Vista try { if (themeSong != null && UserINISettings.Instance.PlayMainMenuMusic) { isMusicFading = false; MediaPlayer.IsRepeating = true; MediaPlayer.Volume = (float)UserINISettings.Instance.ClientVolume; MediaPlayer.Play(themeSong); } } catch (Exception ex) { Logger.Log("Playing main menu music failed! " + ex.ToString()); } } /// /// Lowers the volume of the menu music, or stops playing it if the /// volume is unaudibly low. /// /// Provides a snapshot of timing values. private void FadeMusic(GameTime gameTime) { if (!isMediaPlayerAvailable || !isMusicFading || themeSong == null) return; try { // Fade during 1 second float step = SoundPlayer.Volume * (float)gameTime.ElapsedGameTime.TotalSeconds; if (MediaPlayer.Volume > step) MediaPlayer.Volume -= step; else { MediaPlayer.Stop(); isMusicFading = false; } } catch (Exception ex) { Logger.Log("Fading music failed! Message: " + ex.ToString()); } } /// /// Exits the client. Quickly fades the music if it's playing. /// private void FadeMusicExit() { if (!isMediaPlayerAvailable || themeSong == null) { ExitClient(); return; } try { float step = MEDIA_PLAYER_VOLUME_EXIT_FADE_STEP * (float)UserINISettings.Instance.ClientVolume; if (MediaPlayer.Volume > step) { MediaPlayer.Volume -= step; AddCallback(new Action(FadeMusicExit), null); } else { MediaPlayer.Stop(); ExitClient(); } } catch (Exception ex) { Logger.Log("Fading music on exit failed! Message: " + ex.ToString()); } } private void ExitClient() { Logger.Log("Exiting."); WindowManager.CloseGame(); themeSong?.Dispose(); } public void SwitchOn() { if (UserINISettings.Instance.StopMusicOnMenu) PlayMusic(); if (!ClientConfiguration.Instance.ModMode && UserINISettings.Instance.CheckForUpdates) { // Re-check for updates if ((DateTime.Now - lastUpdateCheckTime) > TimeSpan.FromSeconds(UPDATE_RE_CHECK_THRESHOLD)) CheckForUpdates(); } } public void SwitchOff() { if (UserINISettings.Instance.StopMusicOnMenu) MusicOff(); } private void MusicOff() { try { if (isMediaPlayerAvailable && MediaPlayer.State == MediaState.Playing) { isMusicFading = true; } } catch (Exception ex) { Logger.Log("Turning music off failed! Message: " + ex.ToString()); } } /// /// Checks if media player is available currently. /// It is not available on Windows Vista or other systems without the appropriate media player components. /// /// True if media player is available, false otherwise. private bool IsMediaPlayerAvailable() { try { MediaState state = MediaPlayer.State; return true; } catch (Exception ex) { Logger.Log("Error encountered when checking media player availability. Error message: " + ex.ToString()); return false; } } private void LaunchMapEditor() { OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); using var mapEditorProcess = new Process(); if (osVersion != OSVersion.UNIX) mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MapEditorExePath); else mapEditorProcess.StartInfo.FileName = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.UnixMapEditorExePath); mapEditorProcess.StartInfo.UseShellExecute = false; mapEditorProcess.Start(); } public string GetSwitchName() => "Main Menu".L10N("Client:Main:MainMenu"); } } ================================================ FILE: DXMainClient/DXGUI/Generic/ManualUpdateQueryWindow.cs ================================================ using System; using ClientCore; using ClientCore.Extensions; using ClientGUI; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Generic { /// /// A window that redirects users to manually download an update. /// public class ManualUpdateQueryWindow : XNAWindow { public delegate void ClosedEventHandler(object sender, EventArgs e); public event ClosedEventHandler Closed; public ManualUpdateQueryWindow(WindowManager windowManager) : base(windowManager) { } private XNALabel lblDescription; private string downloadUrl; private string descriptionText; public override void Initialize() { Name = "ManualUpdateQueryWindow"; ClientRectangle = new Rectangle(0, 0, 251, 140); BackgroundTexture = AssetLoader.LoadTexture("updatequerybg.png"); lblDescription = new XNALabel(WindowManager); lblDescription.Name = nameof(lblDescription); lblDescription.ClientRectangle = new Rectangle(12, 9, 0, 0); lblDescription.Text = ("Version {0} is available.\n\nManual download and installation is\nrequired.").L10N("Client:Main:ManualDownloadAvailable"); var btnDownload = new XNAClientButton(WindowManager); btnDownload.Name = nameof(btnDownload); btnDownload.ClientRectangle = new Rectangle(12, 110, 110, 23); btnDownload.Text = "View Downloads".L10N("Client:Main:ButtonViewDownloads"); btnDownload.LeftClick += BtnDownload_LeftClick; var btnClose = new XNAClientButton(WindowManager); btnClose.Name = nameof(btnClose); btnClose.ClientRectangle = new Rectangle(147, 110, 92, 23); btnClose.Text = "Close".L10N("Client:Main:ButtonClose"); btnClose.LeftClick += BtnClose_LeftClick; AddChild(lblDescription); AddChild(btnDownload); AddChild(btnClose); base.Initialize(); // loaded from INI descriptionText = lblDescription.Text; CenterOnParent(); } private void BtnDownload_LeftClick(object sender, EventArgs e) => ProcessLauncher.StartShellProcess(downloadUrl); private void BtnClose_LeftClick(object sender, EventArgs e) => Closed?.Invoke(this, e); public void SetInfo(string version, string downloadUrl) { this.downloadUrl = downloadUrl; lblDescription.Text = string.Format(descriptionText, version); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionPanels/AudioOptionsPanel.cs ================================================ using ClientCore.Extensions; using ClientCore; using ClientGUI; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Generic.OptionPanels { class AudioOptionsPanel : XNAOptionsPanel { private const int VOLUME_MIN = 0; private const int VOLUME_MAX = 10; private const int VOLUME_SCALE = 10; private const int PADDING_X = 12; private const int PADDING_Y = 14; private const int TRACKBAR_X_PADDING = 16; private const int TRACKBAR_Y_PADDING = 16; private const int TRACKBAR_Y_OFFSET = 2; //trackbars sit slightly higher than their labels. private const int TRACKBAR_HEIGHT = 22; private const int CHECKBOX_SPACING = 4; private const int GROUP_SPACING = 22; public AudioOptionsPanel(WindowManager windowManager, UserINISettings iniSettings) : base(windowManager, iniSettings) { } private XNATrackbar trbScoreVolume; private XNATrackbar trbSoundVolume; private XNATrackbar trbVoiceVolume; private XNALabel lblScoreVolumeValue; private XNALabel lblSoundVolumeValue; private XNALabel lblVoiceVolumeValue; private XNAClientCheckBox chkScoreShuffle; private XNALabel lblClientVolumeValue; private XNATrackbar trbClientVolume; private XNAClientCheckBox chkMainMenuMusic; private XNAClientCheckBox chkStopMusicOnMenu; private XNAClientCheckBox chkStopGameLobbyMessageAudio; public override void Initialize() { base.Initialize(); Name = "AudioOptionsPanel"; var lblScoreVolume = new XNALabel(WindowManager); lblScoreVolume.Name = nameof(lblScoreVolume); lblScoreVolume.ClientRectangle = new Rectangle(PADDING_X, PADDING_Y, 0, 0); lblScoreVolume.Text = "Music Volume:".L10N("Client:DTAConfig:MusicVolume"); lblScoreVolumeValue = new XNALabel(WindowManager); lblScoreVolumeValue.Name = nameof(lblScoreVolumeValue); lblScoreVolumeValue.FontIndex = 1; lblScoreVolumeValue.Text = "0"; lblScoreVolumeValue.ClientRectangle = new Rectangle( Width - lblScoreVolumeValue.Width - PADDING_X, lblScoreVolume.Y, 0, 0); trbScoreVolume = new XNATrackbar(WindowManager); trbScoreVolume.Name = nameof(trbScoreVolume); trbScoreVolume.ClientRectangle = new Rectangle( lblScoreVolume.Right + TRACKBAR_X_PADDING, lblScoreVolume.Y - TRACKBAR_Y_OFFSET, lblScoreVolumeValue.X - TRACKBAR_X_PADDING - lblScoreVolume.Right - TRACKBAR_X_PADDING, TRACKBAR_HEIGHT); trbScoreVolume.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2); trbScoreVolume.MinValue = VOLUME_MIN; trbScoreVolume.MaxValue = VOLUME_MAX; trbScoreVolume.ValueChanged += TrbScoreVolume_ValueChanged; var lblSoundVolume = new XNALabel(WindowManager); lblSoundVolume.Name = nameof(lblSoundVolume); lblSoundVolume.ClientRectangle = new Rectangle(lblScoreVolume.X, trbScoreVolume.Bottom + TRACKBAR_Y_PADDING + TRACKBAR_Y_OFFSET, 0, 0); lblSoundVolume.Text = "Sound Volume:".L10N("Client:DTAConfig:SoundVolume"); lblSoundVolumeValue = new XNALabel(WindowManager); lblSoundVolumeValue.Name = nameof(lblSoundVolumeValue); lblSoundVolumeValue.FontIndex = 1; lblSoundVolumeValue.Text = "0"; lblSoundVolumeValue.ClientRectangle = new Rectangle( lblScoreVolumeValue.X, lblSoundVolume.Y, 0, 0); trbSoundVolume = new XNATrackbar(WindowManager); trbSoundVolume.Name = nameof(trbSoundVolume); trbSoundVolume.ClientRectangle = new Rectangle( trbScoreVolume.X, trbScoreVolume.Bottom + TRACKBAR_Y_PADDING, trbScoreVolume.Width, trbScoreVolume.Height); trbSoundVolume.BackgroundTexture = trbScoreVolume.BackgroundTexture; trbSoundVolume.MinValue = VOLUME_MIN; trbSoundVolume.MaxValue = VOLUME_MAX; trbSoundVolume.ValueChanged += TrbSoundVolume_ValueChanged; var lblVoiceVolume = new XNALabel(WindowManager); lblVoiceVolume.Name = nameof(lblVoiceVolume); lblVoiceVolume.ClientRectangle = new Rectangle(lblScoreVolume.X, trbSoundVolume.Bottom + TRACKBAR_Y_PADDING + TRACKBAR_Y_OFFSET, 0, 0); lblVoiceVolume.Text = "Voice Volume:".L10N("Client:DTAConfig:VoiceVolume"); lblVoiceVolumeValue = new XNALabel(WindowManager); lblVoiceVolumeValue.Name = nameof(lblVoiceVolumeValue); lblVoiceVolumeValue.FontIndex = 1; lblVoiceVolumeValue.Text = "0"; lblVoiceVolumeValue.ClientRectangle = new Rectangle( lblScoreVolumeValue.X, lblVoiceVolume.Y, 0, 0); trbVoiceVolume = new XNATrackbar(WindowManager); trbVoiceVolume.Name = nameof(trbVoiceVolume); trbVoiceVolume.ClientRectangle = new Rectangle( trbScoreVolume.X, trbSoundVolume.Bottom + TRACKBAR_Y_PADDING, trbScoreVolume.Width, trbScoreVolume.Height); trbVoiceVolume.BackgroundTexture = trbScoreVolume.BackgroundTexture; trbVoiceVolume.MinValue = VOLUME_MIN; trbVoiceVolume.MaxValue = VOLUME_MAX; trbVoiceVolume.ValueChanged += TrbVoiceVolume_ValueChanged; chkScoreShuffle = new XNAClientCheckBox(WindowManager); chkScoreShuffle.Name = nameof(chkScoreShuffle); chkScoreShuffle.ClientRectangle = new Rectangle( lblScoreVolume.X, trbVoiceVolume.Bottom + TRACKBAR_Y_PADDING, 0, 0); chkScoreShuffle.Text = "Shuffle Music".L10N("Client:DTAConfig:ShuffleMusic"); AddChild(chkScoreShuffle); var lblClientVolume = new XNALabel(WindowManager); lblClientVolume.Name = nameof(lblClientVolume); lblClientVolume.ClientRectangle = new Rectangle(lblScoreVolume.X, chkScoreShuffle.Bottom + GROUP_SPACING + TRACKBAR_Y_OFFSET, 0, 0); lblClientVolume.Text = "Client Volume:".L10N("Client:DTAConfig:ClientVolume"); lblClientVolumeValue = new XNALabel(WindowManager); lblClientVolumeValue.Name = nameof(lblClientVolumeValue); lblClientVolumeValue.FontIndex = 1; lblClientVolumeValue.Text = "0"; lblClientVolumeValue.ClientRectangle = new Rectangle( lblScoreVolumeValue.X, lblClientVolume.Y, 0, 0); trbClientVolume = new XNATrackbar(WindowManager); trbClientVolume.Name = nameof(trbClientVolume); trbClientVolume.ClientRectangle = new Rectangle( trbScoreVolume.X, lblClientVolume.Y - TRACKBAR_Y_OFFSET, trbScoreVolume.Width, trbScoreVolume.Height); trbClientVolume.BackgroundTexture = trbScoreVolume.BackgroundTexture; trbClientVolume.MinValue = VOLUME_MIN; trbClientVolume.MaxValue = VOLUME_MAX; trbClientVolume.ValueChanged += TrbClientVolume_ValueChanged; chkMainMenuMusic = new XNAClientCheckBox(WindowManager); chkMainMenuMusic.Name = nameof(chkMainMenuMusic); chkMainMenuMusic.Text = "Main menu music".L10N("Client:DTAConfig:MainMenuMusic"); chkMainMenuMusic.ClientRectangle = new Rectangle( lblScoreVolume.X, trbClientVolume.Bottom + PADDING_Y, 0, 0); chkMainMenuMusic.CheckedChanged += ChkMainMenuMusic_CheckedChanged; AddChild(chkMainMenuMusic); chkStopMusicOnMenu = new XNAClientCheckBox(WindowManager); chkStopMusicOnMenu.Name = nameof(chkStopMusicOnMenu); chkStopMusicOnMenu.Text = "Don't play main menu music in lobbies".L10N("Client:DTAConfig:NoLobbiesMusic"); chkStopMusicOnMenu.ClientRectangle = new Rectangle( lblScoreVolume.X, chkMainMenuMusic.ClientRectangle.Bottom + CHECKBOX_SPACING, 0, 0); AddChild(chkStopMusicOnMenu); chkStopGameLobbyMessageAudio = new XNAClientCheckBox(WindowManager); chkStopGameLobbyMessageAudio.Name = nameof(chkStopGameLobbyMessageAudio); chkStopGameLobbyMessageAudio.Text = "Don't play lobby message audio when game is running".L10N("Client:DTAConfig:NoGameLobbyMessageAudio"); chkStopGameLobbyMessageAudio.ClientRectangle = new Rectangle( lblScoreVolume.X, chkStopMusicOnMenu.Bottom + CHECKBOX_SPACING, 0, 0); AddChild(chkStopGameLobbyMessageAudio); AddChild(lblScoreVolume); AddChild(lblScoreVolumeValue); AddChild(trbScoreVolume); AddChild(lblSoundVolume); AddChild(lblSoundVolumeValue); AddChild(trbSoundVolume); AddChild(lblVoiceVolume); AddChild(lblVoiceVolumeValue); AddChild(trbVoiceVolume); AddChild(lblClientVolume); AddChild(lblClientVolumeValue); AddChild(trbClientVolume); WindowManager.SoundPlayer.SetVolume(trbClientVolume.Value / (float)VOLUME_SCALE); } private void ChkMainMenuMusic_CheckedChanged(object sender, EventArgs e) { chkStopMusicOnMenu.AllowChecking = chkMainMenuMusic.Checked; chkStopMusicOnMenu.Checked = chkMainMenuMusic.Checked; } private void TrbScoreVolume_ValueChanged(object sender, EventArgs e) { lblScoreVolumeValue.Text = trbScoreVolume.Value.ToString(); } private void TrbSoundVolume_ValueChanged(object sender, EventArgs e) { lblSoundVolumeValue.Text = trbSoundVolume.Value.ToString(); } private void TrbVoiceVolume_ValueChanged(object sender, EventArgs e) { lblVoiceVolumeValue.Text = trbVoiceVolume.Value.ToString(); } private void TrbClientVolume_ValueChanged(object sender, EventArgs e) { lblClientVolumeValue.Text = trbClientVolume.Value.ToString(); WindowManager.SoundPlayer.SetVolume(trbClientVolume.Value / (float)VOLUME_SCALE); } public override void Load() { base.Load(); trbScoreVolume.Value = (int)(IniSettings.ScoreVolume * VOLUME_SCALE); trbSoundVolume.Value = (int)(IniSettings.SoundVolume * VOLUME_SCALE); trbVoiceVolume.Value = (int)(IniSettings.VoiceVolume * VOLUME_SCALE); chkScoreShuffle.Checked = IniSettings.IsScoreShuffle; trbClientVolume.Value = (int)(IniSettings.ClientVolume * VOLUME_SCALE); chkMainMenuMusic.Checked = IniSettings.PlayMainMenuMusic; chkStopMusicOnMenu.Checked = IniSettings.StopMusicOnMenu; chkStopGameLobbyMessageAudio.Checked = IniSettings.StopGameLobbyMessageAudio; } public override bool Save() { bool restartRequired = base.Save(); IniSettings.ScoreVolume.Value = trbScoreVolume.Value / (double)VOLUME_SCALE; IniSettings.SoundVolume.Value = trbSoundVolume.Value / (double)VOLUME_SCALE; IniSettings.VoiceVolume.Value = trbVoiceVolume.Value / (double)VOLUME_SCALE; IniSettings.IsScoreShuffle.Value = chkScoreShuffle.Checked; IniSettings.ClientVolume.Value = trbClientVolume.Value / (double)VOLUME_SCALE; IniSettings.PlayMainMenuMusic.Value = chkMainMenuMusic.Checked; IniSettings.StopMusicOnMenu.Value = chkStopMusicOnMenu.Checked; IniSettings.StopGameLobbyMessageAudio.Value = chkStopGameLobbyMessageAudio.Checked; return restartRequired; } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionPanels/CnCNetOptionsPanel.cs ================================================ using ClientCore.Extensions; using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using ClientGUI; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.Linq; using ClientCore.Enums; namespace DTAClient.DXGUI.Generic.OptionPanels { class CnCNetOptionsPanel : XNAOptionsPanel { public CnCNetOptionsPanel(WindowManager windowManager, UserINISettings iniSettings, GameCollection gameCollection) : base(windowManager, iniSettings) { this.gameCollection = gameCollection; } XNAClientCheckBox chkPingUnofficialTunnels; XNAClientCheckBox chkWriteInstallPathToRegistry; XNAClientCheckBox chkPlaySoundOnGameHosted; XNAClientCheckBox chkNotifyOnUserListChange; XNAClientCheckBox chkSkipLoginWindow; XNAClientCheckBox chkPersistentMode; XNAClientCheckBox chkConnectOnStartup; XNAClientCheckBox chkDiscordIntegration; XNAClientCheckBox chkSteamIntegration; XNAClientCheckBox chkAllowGameInvitesFromFriendsOnly; XNAClientCheckBox chkDisablePrivateMessagePopup; XNAClientDropDown ddAllowPrivateMessagesFrom; GameCollection gameCollection; List followedGameChks = new List(); public override void Initialize() { base.Initialize(); Name = "CnCNetOptionsPanel"; InitOptions(); InitGameListPanel(); } private void InitOptions() { // LEFT COLUMN chkPingUnofficialTunnels = new XNAClientCheckBox(WindowManager); chkPingUnofficialTunnels.Name = nameof(chkPingUnofficialTunnels); chkPingUnofficialTunnels.ClientRectangle = new Rectangle(12, 12, 0, 0); chkPingUnofficialTunnels.Text = "Ping unofficial CnCNet tunnels".L10N("Client:DTAConfig:PingUnofficial"); AddChild(chkPingUnofficialTunnels); chkWriteInstallPathToRegistry = new XNAClientCheckBox(WindowManager); chkWriteInstallPathToRegistry.Name = nameof(chkWriteInstallPathToRegistry); chkWriteInstallPathToRegistry.ClientRectangle = new Rectangle( chkPingUnofficialTunnels.X, chkPingUnofficialTunnels.Bottom + 12, 0, 0); chkWriteInstallPathToRegistry.Text = ("Write game installation path to Windows\n" + "Registry (makes it possible to join\n" + "other games' game rooms on CnCNet)").L10N("Client:DTAConfig:WriteGameRegistry"); AddChild(chkWriteInstallPathToRegistry); chkPlaySoundOnGameHosted = new XNAClientCheckBox(WindowManager); chkPlaySoundOnGameHosted.Name = nameof(chkPlaySoundOnGameHosted); chkPlaySoundOnGameHosted.ClientRectangle = new Rectangle( chkPingUnofficialTunnels.X, chkWriteInstallPathToRegistry.Bottom + 12, 0, 0); chkPlaySoundOnGameHosted.Text = "Play sound when a game is hosted".L10N("Client:DTAConfig:PlaySoundGameHosted"); AddChild(chkPlaySoundOnGameHosted); chkNotifyOnUserListChange = new XNAClientCheckBox(WindowManager); chkNotifyOnUserListChange.Name = nameof(chkNotifyOnUserListChange); chkNotifyOnUserListChange.ClientRectangle = new Rectangle( chkPingUnofficialTunnels.X, chkPlaySoundOnGameHosted.Bottom + 12, 0, 0); chkNotifyOnUserListChange.Text = ("Show player join / quit messages\n" + "on CnCNet lobby").L10N("Client:DTAConfig:ShowPlayerJoinQuit"); AddChild(chkNotifyOnUserListChange); chkDisablePrivateMessagePopup = new XNAClientCheckBox(WindowManager); chkDisablePrivateMessagePopup.Name = nameof(chkDisablePrivateMessagePopup); chkDisablePrivateMessagePopup.ClientRectangle = new Rectangle( chkNotifyOnUserListChange.X, chkNotifyOnUserListChange.Bottom + 8, 0, 0); chkDisablePrivateMessagePopup.Text = "Disable Popups from Private Messages".L10N("Client:DTAConfig:DisablePMPopup"); AddChild(chkDisablePrivateMessagePopup); InitAllowPrivateMessagesFromDropdown(); // RIGHT COLUMN chkSkipLoginWindow = new XNAClientCheckBox(WindowManager); chkSkipLoginWindow.Name = nameof(chkSkipLoginWindow); chkSkipLoginWindow.ClientRectangle = new Rectangle( 276, 12, 0, 0); chkSkipLoginWindow.Text = "Skip login dialog".L10N("Client:DTAConfig:SkipLoginDialog"); chkSkipLoginWindow.CheckedChanged += ChkSkipLoginWindow_CheckedChanged; AddChild(chkSkipLoginWindow); chkPersistentMode = new XNAClientCheckBox(WindowManager); chkPersistentMode.Name = nameof(chkPersistentMode); chkPersistentMode.ClientRectangle = new Rectangle( chkSkipLoginWindow.X, chkSkipLoginWindow.Bottom + 12, 0, 0); chkPersistentMode.Text = "Stay connected outside of the CnCNet lobby".L10N("Client:DTAConfig:StayConnect"); chkPersistentMode.CheckedChanged += ChkPersistentMode_CheckedChanged; AddChild(chkPersistentMode); chkConnectOnStartup = new XNAClientCheckBox(WindowManager); chkConnectOnStartup.Name = nameof(chkConnectOnStartup); chkConnectOnStartup.ClientRectangle = new Rectangle( chkSkipLoginWindow.X, chkPersistentMode.Bottom + 12, 0, 0); chkConnectOnStartup.Text = "Connect automatically on client startup".L10N("Client:DTAConfig:ConnectOnStart"); chkConnectOnStartup.AllowChecking = false; AddChild(chkConnectOnStartup); chkDiscordIntegration = new XNAClientCheckBox(WindowManager); chkDiscordIntegration.Name = nameof(chkDiscordIntegration); chkDiscordIntegration.ClientRectangle = new Rectangle( chkSkipLoginWindow.X, chkConnectOnStartup.Bottom + 12, 0, 0); chkDiscordIntegration.Text = "Show detailed game info in Discord status".L10N("Client:DTAConfig:DiscordStatus"); if (ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) { chkDiscordIntegration.AllowChecking = false; chkDiscordIntegration.Checked = false; } else { chkDiscordIntegration.AllowChecking = true; } AddChild(chkDiscordIntegration); chkAllowGameInvitesFromFriendsOnly = new XNAClientCheckBox(WindowManager); chkAllowGameInvitesFromFriendsOnly.Name = nameof(chkAllowGameInvitesFromFriendsOnly); chkAllowGameInvitesFromFriendsOnly.ClientRectangle = new Rectangle( chkDiscordIntegration.X, chkDiscordIntegration.Bottom + 12, 0, 0); chkAllowGameInvitesFromFriendsOnly.Text = "Only receive game invitations from friends".L10N("Client:DTAConfig:FriendsOnly"); AddChild(chkAllowGameInvitesFromFriendsOnly); chkSteamIntegration = new XNAClientCheckBox(WindowManager); chkSteamIntegration.Name = nameof(chkSteamIntegration); chkSteamIntegration.ClientRectangle = new Rectangle( chkAllowGameInvitesFromFriendsOnly.X, chkAllowGameInvitesFromFriendsOnly.Bottom + 12, 0, 0); chkSteamIntegration.Text = "Show the game being played in Steam".L10N("Client:DTAConfig:SteamStatus"); AddChild(chkSteamIntegration); } private void InitAllowPrivateMessagesFromDropdown() { XNALabel lblAllPrivateMessagesFrom = new XNALabel(WindowManager); lblAllPrivateMessagesFrom.Name = nameof(lblAllPrivateMessagesFrom); lblAllPrivateMessagesFrom.Text = "Allow Private Messages From:".L10N("Client:DTAConfig:AllowPMFrom"); lblAllPrivateMessagesFrom.ClientRectangle = new Rectangle( chkDisablePrivateMessagePopup.X, chkDisablePrivateMessagePopup.Bottom + 12, 165, 0); AddChild(lblAllPrivateMessagesFrom); ddAllowPrivateMessagesFrom = new XNAClientDropDown(WindowManager); ddAllowPrivateMessagesFrom.Name = nameof(ddAllowPrivateMessagesFrom); ddAllowPrivateMessagesFrom.ClientRectangle = new Rectangle( lblAllPrivateMessagesFrom.Right, lblAllPrivateMessagesFrom.Y - 2, 110, 0); ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem() { Text = "All".L10N("Client:DTAConfig:PMAll"), Tag = AllowPrivateMessagesFromEnum.All, }); ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem() { Text = "Current channel".L10N("Client:DTAConfig:PMCurrentChannel"), Tag = AllowPrivateMessagesFromEnum.CurrentChannel, }); ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem() { Text = "Friends".L10N("Client:DTAConfig:PMFriends"), Tag = AllowPrivateMessagesFromEnum.Friends, }); ddAllowPrivateMessagesFrom.AddItem(new XNADropDownItem() { Text = "None".L10N("Client:DTAConfig:PMNone"), Tag = AllowPrivateMessagesFromEnum.None, }); AddChild(ddAllowPrivateMessagesFrom); } private void InitGameListPanel() { const int gameListPanelHeight = 185; XNAPanel gameListPanel = new XNAPanel(WindowManager); gameListPanel.DrawBorders = false; gameListPanel.Name = nameof(gameListPanel); gameListPanel.ClientRectangle = new Rectangle(0, Bottom - gameListPanelHeight, Width, gameListPanelHeight); AddChild(gameListPanel); var lblFollowedGames = new XNALabel(WindowManager); lblFollowedGames.Name = nameof(lblFollowedGames); lblFollowedGames.ClientRectangle = new Rectangle(12, 12, 0, 0); lblFollowedGames.Text = "Show game rooms from the following games:".L10N("Client:DTAConfig:ShowRoomFromGame"); gameListPanel.AddChild(lblFollowedGames); // Max number of games per column const int maxGamesPerColumn = 4; // Spacing buffer between columns const int columnBuffer = 20; // Spacing buffer between rows const int rowBuffer = 22; // Render width of a game icon const int gameIconWidth = 16; // Spacing buffer between game icon and game check box const int gameIconBuffer = 6; // List of supported games IEnumerable supportedGames = gameCollection.GameList .Where(game => game.Supported && !string.IsNullOrEmpty(game.GameBroadcastChannel)); // Convert to a matrix of XNAPanels that contain the game icons and check boxes List> gamePanelMatrix = supportedGames .Select(game => { var gameIconPanel = new XNAPanel(WindowManager); gameIconPanel.Name = "gameIcon" + game.InternalName.ToUpperInvariant(); gameIconPanel.ClientRectangle = new Rectangle(0, 0, gameIconWidth, gameIconWidth); gameIconPanel.DrawBorders = false; gameIconPanel.BackgroundTexture = game.Texture; var gameChkBox = new XNAClientCheckBox(WindowManager); gameChkBox.Name = game.InternalName.ToUpperInvariant(); gameChkBox.ClientRectangle = new Rectangle(gameIconPanel.Right + gameIconBuffer, 0, 0, 0); gameChkBox.Text = game.UIName; var gamePanel = new XNAPanel(WindowManager); gamePanel.AddChild(gameIconPanel); gamePanel.AddChild(gameChkBox); gamePanel.Name = "gamePanel" + game.InternalName.ToUpperInvariant(); gamePanel.DrawBorders = false; gamePanel.ClientRectangle = new Rectangle(lblFollowedGames.X, 0, gameIconPanel.Width + gameChkBox.Width + gameIconBuffer, gameIconPanel.Height); followedGameChks.Add(gameChkBox); return gamePanel; }) .ToMatrix(maxGamesPerColumn); // Calculate max widths for each column List columnWidths = gamePanelMatrix .Select(columnList => columnList.Max(gamePanel => gamePanel.Children.Last().Right + columnBuffer)) .ToList(); // Reposition each game panel and then add them to the overall list panel int startY = lblFollowedGames.Bottom + 12; for (int col = 0; col < gamePanelMatrix.Count; col++) { List gamePanelColumn = gamePanelMatrix[col]; for (int row = 0; row < gamePanelColumn.Count; row++) { int columnOffset = columnWidths.Take(col).Sum(); int rowOffset = startY + row * rowBuffer; XNAPanel gamePanel = gamePanelColumn[row]; gamePanel.ClientRectangle = new Rectangle(gamePanel.X + columnOffset, rowOffset, gamePanel.Width, gamePanel.Height); gameListPanel.AddChild(gamePanel); } } } private void ChkSkipLoginWindow_CheckedChanged(object sender, EventArgs e) { CheckConnectOnStartupAllowance(); } private void ChkPersistentMode_CheckedChanged(object sender, EventArgs e) { CheckConnectOnStartupAllowance(); } private void CheckConnectOnStartupAllowance() { if (!chkSkipLoginWindow.Checked || !chkPersistentMode.Checked) { chkConnectOnStartup.AllowChecking = false; chkConnectOnStartup.Checked = false; return; } chkConnectOnStartup.AllowChecking = true; } public override void Load() { base.Load(); chkPingUnofficialTunnels.Checked = IniSettings.PingUnofficialCnCNetTunnels; chkWriteInstallPathToRegistry.Checked = IniSettings.WritePathToRegistry; chkPlaySoundOnGameHosted.Checked = IniSettings.PlaySoundOnGameHosted; chkNotifyOnUserListChange.Checked = IniSettings.NotifyOnUserListChange; chkDisablePrivateMessagePopup.Checked = IniSettings.DisablePrivateMessagePopups; SetAllowPrivateMessagesFromState(IniSettings.AllowPrivateMessagesFromState); chkConnectOnStartup.Checked = IniSettings.AutomaticCnCNetLogin; chkSkipLoginWindow.Checked = IniSettings.SkipConnectDialog; chkPersistentMode.Checked = IniSettings.PersistentMode; chkSteamIntegration.Checked = IniSettings.SteamIntegration; chkDiscordIntegration.Checked = !ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled && IniSettings.DiscordIntegration; chkAllowGameInvitesFromFriendsOnly.Checked = IniSettings.AllowGameInvitesFromFriendsOnly; string localGame = ClientConfiguration.Instance.LocalGame.ToUpperInvariant(); foreach (var chkBox in followedGameChks) { if (chkBox.Name == localGame) { chkBox.AllowChecking = false; chkBox.Checked = true; IniSettings.SettingsIni.SetBooleanValue("Channels", localGame, true); continue; } chkBox.Checked = IniSettings.IsGameFollowed(chkBox.Name); } } public override bool Save() { bool restartRequired = base.Save(); IniSettings.PingUnofficialCnCNetTunnels.Value = chkPingUnofficialTunnels.Checked; IniSettings.WritePathToRegistry.Value = chkWriteInstallPathToRegistry.Checked; IniSettings.PlaySoundOnGameHosted.Value = chkPlaySoundOnGameHosted.Checked; IniSettings.NotifyOnUserListChange.Value = chkNotifyOnUserListChange.Checked; IniSettings.DisablePrivateMessagePopups.Value = chkDisablePrivateMessagePopup.Checked; IniSettings.AllowPrivateMessagesFromState.Value = GetAllowPrivateMessagesFromState(); IniSettings.AutomaticCnCNetLogin.Value = chkConnectOnStartup.Checked; IniSettings.SkipConnectDialog.Value = chkSkipLoginWindow.Checked; IniSettings.PersistentMode.Value = chkPersistentMode.Checked; IniSettings.SteamIntegration.Value = chkSteamIntegration.Checked; if (!ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) { IniSettings.DiscordIntegration.Value = chkDiscordIntegration.Checked; } IniSettings.AllowGameInvitesFromFriendsOnly.Value = chkAllowGameInvitesFromFriendsOnly.Checked; foreach (var chkBox in followedGameChks) { IniSettings.SettingsIni.SetBooleanValue("Channels", chkBox.Name, chkBox.Checked); } return restartRequired; } private void SetAllowPrivateMessagesFromState(int state) { var selectedIndex = ddAllowPrivateMessagesFrom.Items.FindIndex(i => (int)i.Tag == state); if (selectedIndex < 0) selectedIndex = ddAllowPrivateMessagesFrom.Items.FindIndex(i => (AllowPrivateMessagesFromEnum)i.Tag == AllowPrivateMessagesFromEnum.All); ddAllowPrivateMessagesFrom.SelectedIndex = selectedIndex; } private int GetAllowPrivateMessagesFromState() { return (int)(ddAllowPrivateMessagesFrom.SelectedItem?.Tag ?? AllowPrivateMessagesFromEnum.All); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionPanels/ComponentsPanel.cs ================================================ using ClientCore.Extensions; using ClientCore; using ClientGUI; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using ClientUpdater; namespace DTAClient.DXGUI.Generic.OptionPanels { class ComponentsPanel : XNAOptionsPanel { public ComponentsPanel(WindowManager windowManager, UserINISettings iniSettings) : base(windowManager, iniSettings) { } List installationButtons = new List(); bool downloadCancelled = false; public override void Initialize() { base.Initialize(); Name = "ComponentsPanel"; int componentIndex = 0; if (Updater.CustomComponents == null) return; foreach (CustomComponent c in Updater.CustomComponents) { string buttonText = "Not Available".L10N("Client:DTAConfig:NotAvailable"); if (SafePath.GetFile(ProgramConstants.GamePath, c.LocalPath).Exists) { buttonText = "Uninstall".L10N("Client:DTAConfig:Uninstall"); if (c.LocalIdentifier != c.RemoteIdentifier) buttonText = "Update".L10N("Client:DTAConfig:Update"); } else { if (!string.IsNullOrEmpty(c.RemoteIdentifier)) buttonText = "Install".L10N("Client:DTAConfig:Install"); } XNAClientButton btn = new(WindowManager) { Name = "btn" + c.ININame, ClientRectangle = new Rectangle(Width - 145, 12 + componentIndex * 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT), Text = buttonText, Tag = c }; btn.LeftClick += Btn_LeftClick; XNALabel lbl = new(WindowManager) { Name = "lbl" + c.ININame, ClientRectangle = new Rectangle(12, btn.Y + 2, 0, 0), Text = c.GUIName }; AddChild(btn); AddChild(lbl); installationButtons.Add(btn); componentIndex++; } Updater.FileIdentifiersUpdated += Updater_FileIdentifiersUpdated; } private void Updater_FileIdentifiersUpdated() => UpdateInstallationButtons(); public override void Load() { base.Load(); UpdateInstallationButtons(); } private void UpdateInstallationButtons() { if (Updater.CustomComponents == null) return; int componentIndex = 0; foreach (CustomComponent c in Updater.CustomComponents) { if (!c.Initialized || c.IsBeingDownloaded) { installationButtons[componentIndex].AllowClick = false; componentIndex++; continue; } string buttonText = "Not Available".L10N("Client:DTAConfig:NotAvailable"); bool buttonEnabled = false; if (SafePath.GetFile(ProgramConstants.GamePath, c.LocalPath).Exists) { buttonText = "Uninstall".L10N("Client:DTAConfig:Uninstall"); buttonEnabled = true; if (c.LocalIdentifier != c.RemoteIdentifier) buttonText = "Update".L10N("Client:DTAConfig:Update") + $" ({GetSizeString(c.RemoteSize)})"; } else { if (!string.IsNullOrEmpty(c.RemoteIdentifier)) { buttonText = "Install".L10N("Client:DTAConfig:Install") + $" ({GetSizeString(c.RemoteSize)})"; buttonEnabled = true; } } installationButtons[componentIndex].Text = buttonText; installationButtons[componentIndex].AllowClick = buttonEnabled; componentIndex++; } } private void Btn_LeftClick(object sender, EventArgs e) { var btn = (XNAClientButton)sender; var cc = (CustomComponent)btn.Tag; if (cc.IsBeingDownloaded) return; FileInfo localFileInfo = SafePath.GetFile(ProgramConstants.GamePath, cc.LocalPath); if (localFileInfo.Exists) { if (cc.LocalIdentifier == cc.RemoteIdentifier) { localFileInfo.IsReadOnly = false; localFileInfo.Delete(); btn.Text = "Install".L10N("Client:DTAConfig:Install") + $" ({GetSizeString(cc.RemoteSize)})"; return; } btn.AllowClick = false; cc.DownloadFinished += cc_DownloadFinished; cc.DownloadProgressChanged += cc_DownloadProgressChanged; cc.DownloadComponent(); } else { var msgBox = new XNAMessageBox(WindowManager, "Confirmation Required".L10N("Client:DTAConfig:UpdateConfirmRequiredTitle"), string.Format(("To enable {0} the Client will need to download the necessary files to your game directory.\n\n" + "This will take an additional {1} of disk space, and the download may take some time\n" + "depending on your Internet connection speed. The size of the download is {2}.\n\n" + "You will not be able to play during the download. Do you wish to continue?").L10N("Client:DTAConfig:UpdateConfirmRequiredText"), cc.GUIName, GetSizeString(cc.RemoteSize), GetSizeString(cc.Archived ? cc.RemoteArchiveSize : cc.RemoteSize)), XNAMessageBoxButtons.YesNo); msgBox.Tag = btn; msgBox.Show(); msgBox.YesClickedAction = MsgBox_YesClicked; } } private void MsgBox_YesClicked(XNAMessageBox messageBox) { var btn = (XNAClientButton)messageBox.Tag; btn.AllowClick = false; var cc = (CustomComponent)btn.Tag; cc.DownloadFinished += cc_DownloadFinished; cc.DownloadProgressChanged += cc_DownloadProgressChanged; cc.DownloadComponent(); } public void InstallComponent(int id) { var btn = installationButtons[id]; btn.AllowClick = false; var cc = (CustomComponent)btn.Tag; cc.DownloadFinished += cc_DownloadFinished; cc.DownloadProgressChanged += cc_DownloadProgressChanged; cc.DownloadComponent(); } /// /// Called whenever a custom component download's progress is changed. /// /// The CustomComponent object. /// The current download progress percentage. private void cc_DownloadProgressChanged(CustomComponent c, int percentage) { WindowManager.AddCallback(new Action(HandleDownloadProgressChanged), c, percentage); } private void HandleDownloadProgressChanged(CustomComponent cc, int percentage) { percentage = Math.Min(percentage, 100); var btn = installationButtons.Find(b => object.ReferenceEquals(b.Tag, cc)); if (cc.Archived && percentage == 100) btn.Text = "Unpacking...".L10N("Client:DTAConfig:Unpacking"); else btn.Text = "Downloading...".L10N("Client:DTAConfig:Downloading") + " " + percentage + "%"; } /// /// Called whenever a custom component download is finished. /// /// The CustomComponent object. /// True if the download succeeded, otherwise false. private void cc_DownloadFinished(CustomComponent c, bool success) { WindowManager.AddCallback(new Action(HandleDownloadFinished), c, success); } private void HandleDownloadFinished(CustomComponent cc, bool success) { cc.DownloadFinished -= cc_DownloadFinished; cc.DownloadProgressChanged -= cc_DownloadProgressChanged; var btn = installationButtons.Find(b => object.ReferenceEquals(b.Tag, cc)); btn.AllowClick = true; if (!success) { if (!downloadCancelled) { XNAMessageBox.Show(WindowManager, "Optional Component Download Failed".L10N("Client:DTAConfig:OptionalComponentDownloadFailedTitle"), string.Format(("Download of optional component {0} failed.\n" + "See client.log for details.\n\n" + "If this problem continues, please contact your mod's authors for support.").L10N("Client:DTAConfig:OptionalComponentDownloadFailedText"), cc.GUIName)); } btn.Text = "Install".L10N("Client:DTAConfig:Install") + $" ({GetSizeString(cc.RemoteSize)})"; if (SafePath.GetFile(ProgramConstants.GamePath, cc.LocalPath).Exists) btn.Text = "Update".L10N("Client:DTAConfig:Update") + $" ({GetSizeString(cc.RemoteSize)})"; } else { XNAMessageBox.Show(WindowManager, "Download Completed".L10N("Client:DTAConfig:DownloadCompleteTitle"), string.Format("Download of optional component {0} completed succesfully.".L10N("Client:DTAConfig:DownloadCompleteText"), cc.GUIName)); btn.Text = "Uninstall".L10N("Client:DTAConfig:Uninstall"); } } public void CancelAllDownloads() { Logger.Log("Cancelling all custom component downloads."); downloadCancelled = true; if (Updater.CustomComponents == null) return; foreach (CustomComponent cc in Updater.CustomComponents) { if (cc.IsBeingDownloaded) cc.StopDownload(); } } public void Open() { downloadCancelled = false; } private string GetSizeString(long size) { if (size < 1048576) { return (size / 1024) + " KB"; } else { return (size / 1048576) + " MB"; } } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionPanels/DisplayOptionsPanel.cs ================================================ using ClientCore.Extensions; using ClientCore; using ClientGUI; using DTAClient.Domain; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; #if WINFORMS using System.Windows.Forms; #endif using Microsoft.Win32; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.IO; using ClientCore.I18N; using ClientCore.Enums; using System.Diagnostics; using System.Linq; namespace DTAClient.DXGUI.Generic.OptionPanels { class DisplayOptionsPanel : XNAOptionsPanel { // Mouse must move at least this many pixels from click point before drag selection activates. private const int DRAG_DISTANCE_DEFAULT = 4; private const int ORIGINAL_RESOLUTION_WIDTH = 640; private readonly DirectDrawWrapperManager directDrawWrapperManager; public DisplayOptionsPanel(WindowManager windowManager, UserINISettings iniSettings, DirectDrawWrapperManager directDrawWrapperManager) : base(windowManager, iniSettings) { this.directDrawWrapperManager = directDrawWrapperManager; } private XNAClientDropDown ddIngameResolution; private XNAClientDropDown ddDetailLevel; private XNAClientDropDown ddRenderer; private XNAClientCheckBox chkWindowedMode; private XNAClientCheckBox chkBorderlessWindowedMode; private XNAClientCheckBox chkBackBufferInVRAM; private XNAClientPreferredItemDropDown ddClientResolution; private XNAClientCheckBox chkBorderlessClient; private XNAClientCheckBox chkIntegerScaledClient; private XNAClientDropDown ddClientTheme; private XNAClientDropDown ddTranslation; private XNALabel lblCompatibilityFixes; private XNALabel lblGameCompatibilityFix; private XNALabel lblMapEditorCompatibilityFix; private XNAClientButton btnGameCompatibilityFix; private XNAClientButton btnMapEditorCompatibilityFix; private bool GameCompatFixInstalled = false; private bool FinalSunCompatFixInstalled = false; private bool GameCompatFixDeclined = false; //private bool FinalSunCompatFixDeclined = false; public override void Initialize() { base.Initialize(); Name = "DisplayOptionsPanel"; var lblIngameResolution = new XNALabel(WindowManager); lblIngameResolution.Name = nameof(lblIngameResolution); lblIngameResolution.ClientRectangle = new Rectangle(12, 14, 0, 0); lblIngameResolution.Text = "In-game Resolution:".L10N("Client:DTAConfig:InGameResolution"); ddIngameResolution = new XNAClientDropDown(WindowManager); ddIngameResolution.Name = nameof(ddIngameResolution); ddIngameResolution.ClientRectangle = new Rectangle( lblIngameResolution.Right + 12, lblIngameResolution.Y - 2, 120, 19); // Add in-game resolutions { var maximumIngameResolution = new ScreenResolution(ClientConfiguration.Instance.MaximumIngameWidth, ClientConfiguration.Instance.MaximumIngameHeight); #if XNA if (!ScreenResolution.HiDefLimitResolution.Fits(maximumIngameResolution)) maximumIngameResolution = ScreenResolution.HiDefLimitResolution; #endif SortedSet resolutions = ScreenResolution.GetFullScreenResolutions( ClientConfiguration.Instance.MinimumIngameWidth, ClientConfiguration.Instance.MinimumIngameHeight, maximumIngameResolution.Width, maximumIngameResolution.Height); foreach (var res in resolutions) ddIngameResolution.AddItem(res.ToString()); } var lblDetailLevel = new XNALabel(WindowManager); lblDetailLevel.Name = nameof(lblDetailLevel); lblDetailLevel.ClientRectangle = new Rectangle(lblIngameResolution.X, ddIngameResolution.Bottom + 16, 0, 0); lblDetailLevel.Text = "Detail Level:".L10N("Client:DTAConfig:DetailLevel"); ddDetailLevel = new XNAClientDropDown(WindowManager); ddDetailLevel.Name = nameof(ddDetailLevel); ddDetailLevel.ClientRectangle = new Rectangle( ddIngameResolution.X, lblDetailLevel.Y - 2, ddIngameResolution.Width, ddIngameResolution.Height); ddDetailLevel.AddItem("Low".L10N("Client:DTAConfig:DetailLevelLow")); ddDetailLevel.AddItem("Medium".L10N("Client:DTAConfig:DetailLevelMedium")); ddDetailLevel.AddItem("High".L10N("Client:DTAConfig:DetailLevelHigh")); var lblRenderer = new XNALabel(WindowManager); lblRenderer.Name = nameof(lblRenderer); lblRenderer.ClientRectangle = new Rectangle(lblDetailLevel.X, ddDetailLevel.Bottom + 16, 0, 0); lblRenderer.Text = "Renderer:".L10N("Client:DTAConfig:Renderer"); ddRenderer = new XNAClientDropDown(WindowManager); ddRenderer.Name = nameof(ddRenderer); ddRenderer.ClientRectangle = new Rectangle( ddDetailLevel.X, lblRenderer.Y - 2, ddDetailLevel.Width, ddDetailLevel.Height); foreach (var renderer in directDrawWrapperManager.GetRenderers(ClientConfiguration.Instance.GetOperatingSystemVersion())) { ddRenderer.AddItem(new XNADropDownItem() { Text = renderer.UIName, Tag = renderer }); } chkWindowedMode = new XNAClientCheckBox(WindowManager); chkWindowedMode.Name = nameof(chkWindowedMode); chkWindowedMode.ClientRectangle = new Rectangle(lblDetailLevel.X, ddRenderer.Bottom + 16, 0, 0); chkWindowedMode.Text = "Windowed Mode".L10N("Client:DTAConfig:WindowedMode"); chkWindowedMode.CheckedChanged += ChkWindowedMode_CheckedChanged; chkBorderlessWindowedMode = new XNAClientCheckBox(WindowManager); chkBorderlessWindowedMode.Name = nameof(chkBorderlessWindowedMode); chkBorderlessWindowedMode.ClientRectangle = new Rectangle( chkWindowedMode.X + 50, chkWindowedMode.Bottom + 24, 0, 0); chkBorderlessWindowedMode.Text = "Borderless Windowed Mode".L10N("Client:DTAConfig:BorderlessWindowedMode"); chkBorderlessWindowedMode.AllowChecking = false; chkBackBufferInVRAM = new XNAClientCheckBox(WindowManager); chkBackBufferInVRAM.Name = nameof(chkBackBufferInVRAM); chkBackBufferInVRAM.ClientRectangle = new Rectangle( lblDetailLevel.X, chkBorderlessWindowedMode.Bottom + 28, 0, 0); chkBackBufferInVRAM.Text = ("Back Buffer in Video Memory\n(lower performance, but is\nnecessary on some systems)").L10N("Client:DTAConfig:BackBuffer"); var lblClientResolution = new XNALabel(WindowManager); lblClientResolution.Name = nameof(lblClientResolution); lblClientResolution.ClientRectangle = new Rectangle( 285, 14, 0, 0); lblClientResolution.Text = "Client Resolution:".L10N("Client:DTAConfig:ClientResolution"); ddClientResolution = new XNAClientPreferredItemDropDown(WindowManager); ddClientResolution.Name = nameof(ddClientResolution); ddClientResolution.ClientRectangle = new Rectangle( lblClientResolution.Right + 12, lblClientResolution.Y - 2, Width - (lblClientResolution.Right + 24), ddIngameResolution.Height); ddClientResolution.AllowDropDown = false; ddClientResolution.PreferredItemLabel = "(recommended)".L10N("Client:DTAConfig:Recommended"); // Add client resolutions { SortedSet scaledRecommendedResolutions = ScreenResolution.GetRecommendedResolutions(); SortedSet resolutions = [ .. ScreenResolution.GetFullScreenResolutions(minWidth: 800, minHeight: 600), .. ScreenResolution.GetWindowedResolutions(minWidth: 800, minHeight: 600), .. scaledRecommendedResolutions, ]; List resolutionList = resolutions.ToList(); foreach (ScreenResolution res in resolutionList) { var item = new XNADropDownItem(); item.Text = res.ToString(); item.Tag = res.ToString(); ddClientResolution.AddItem(item); } // So we add the optimal resolutions to the list, sort it and then find // out the optimal resolution index - it's inefficient, but works // Note: ddClientResolution.PreferredItemIndexes is assumed in ascending order foreach (ScreenResolution scaledRecommendedResolution in scaledRecommendedResolutions) { int index = resolutionList.FindIndex(res => res == scaledRecommendedResolution); if (index > -1) ddClientResolution.PreferredItemIndexes.Add(index); } } chkBorderlessClient = new XNAClientCheckBox(WindowManager); chkBorderlessClient.Name = nameof(chkBorderlessClient); chkBorderlessClient.ClientRectangle = new Rectangle( lblClientResolution.X, lblDetailLevel.Y, 0, 0); chkBorderlessClient.Text = "Fullscreen Client".L10N("Client:DTAConfig:FullscreenClient"); chkBorderlessClient.CheckedChanged += ChkBorderlessMenu_CheckedChanged; chkBorderlessClient.Checked = true; chkIntegerScaledClient = new XNAClientCheckBox(WindowManager); chkIntegerScaledClient.Name = nameof(chkIntegerScaledClient); chkIntegerScaledClient.ClientRectangle = new Rectangle( lblClientResolution.X, lblRenderer.Y, 0, 0); chkIntegerScaledClient.Text = "Integer Scaled Client".L10N("Client:DTAConfig:IntegerScaledClient"); chkIntegerScaledClient.Checked = IniSettings.IntegerScaledClient.Value; chkIntegerScaledClient.ToolTipText = """ Enable integer scaling for the client. This will cause the client to use the closest fitting resolution that is required to maintain sharp graphics, at the expense of black borders that may appear at some resolutions. Additionally, enabling this option will also allow the client window to be resized (does not affect the selected client resolution). """ .L10N("Client:DTAConfig:IntegerScaledClientToolTip"); var lblClientTheme = new XNALabel(WindowManager); lblClientTheme.Name = nameof(lblClientTheme); lblClientTheme.ClientRectangle = new Rectangle( lblClientResolution.X, chkWindowedMode.Y, 0, 0); lblClientTheme.Text = "Client Theme:".L10N("Client:DTAConfig:ClientTheme"); ddClientTheme = new XNAClientDropDown(WindowManager); ddClientTheme.Name = nameof(ddClientTheme); ddClientTheme.ClientRectangle = new Rectangle( ddClientResolution.X, chkWindowedMode.Y, ddClientResolution.Width, ddRenderer.Height); int themeCount = ClientConfiguration.Instance.ThemeCount; for (int i = 0; i < themeCount; i++) { string themeName = ClientConfiguration.Instance.GetThemeInfoFromIndex(i).Name; string displayName = themeName.L10N($"INI:Themes:{themeName}"); ddClientTheme.AddItem(new XNADropDownItem { Text = displayName, Tag = themeName }); } var lblTranslation = new XNALabel(WindowManager); lblTranslation.Name = nameof(lblTranslation); lblTranslation.ClientRectangle = new Rectangle( lblClientTheme.X, ddClientTheme.Bottom + 16, 0, 0); lblTranslation.Text = "Language:".L10N("Client:DTAConfig:Language"); ddTranslation = new XNAClientDropDown(WindowManager); ddTranslation.Name = nameof(ddTranslation); ddTranslation.ClientRectangle = new Rectangle( ddClientTheme.X, lblTranslation.Y - 2, ddClientTheme.Width, ddClientTheme.Height); foreach (var (translation, name) in Translation.GetTranslations()) ddTranslation.AddItem(new XNADropDownItem { Text = name, Tag = translation }); if (ClientConfiguration.Instance.ClientGameType == ClientType.TS && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { AddCompatibilityFixControls(); } AddChild(chkWindowedMode); AddChild(chkBorderlessWindowedMode); AddChild(chkBackBufferInVRAM); AddChild(chkBorderlessClient); AddChild(chkIntegerScaledClient); AddChild(lblClientTheme); AddChild(ddClientTheme); AddChild(lblTranslation); AddChild(ddTranslation); AddChild(lblClientResolution); AddChild(ddClientResolution); AddChild(lblRenderer); AddChild(ddRenderer); AddChild(lblDetailLevel); AddChild(ddDetailLevel); AddChild(lblIngameResolution); AddChild(ddIngameResolution); } [SupportedOSPlatform("windows")] private void AddCompatibilityFixControls() { lblCompatibilityFixes = new XNALabel(WindowManager); lblCompatibilityFixes.Name = "lblCompatibilityFixes"; lblCompatibilityFixes.FontIndex = 1; lblCompatibilityFixes.Text = "Legacy Compatibility Fixes:".L10N("Client:DTAConfig:TSCompatibilityFixLegacy"); AddChild(lblCompatibilityFixes); lblCompatibilityFixes.CenterOnParent(); lblCompatibilityFixes.Y = Height - 97; lblGameCompatibilityFix = new XNALabel(WindowManager); lblGameCompatibilityFix.Name = "lblGameCompatibilityFix"; lblGameCompatibilityFix.ClientRectangle = new Rectangle(132, lblCompatibilityFixes.Bottom + 20, 0, 0); lblGameCompatibilityFix.Text = "DTA/TI/TS Compatibility Fix:".L10N("Client:DTAConfig:TSCompatibilityFix"); btnGameCompatibilityFix = new XNAClientButton(WindowManager); btnGameCompatibilityFix.Name = "btnGameCompatibilityFix"; btnGameCompatibilityFix.ClientRectangle = new Rectangle( lblGameCompatibilityFix.Right + 20, lblGameCompatibilityFix.Y - 4, 133, 23); btnGameCompatibilityFix.FontIndex = 1; btnGameCompatibilityFix.Text = "Disable".L10N("Client:DTAConfig:TSDisable"); btnGameCompatibilityFix.LeftClick += BtnGameCompatibilityFix_LeftClick; lblMapEditorCompatibilityFix = new XNALabel(WindowManager); lblMapEditorCompatibilityFix.Name = "lblMapEditorCompatibilityFix"; lblMapEditorCompatibilityFix.ClientRectangle = new Rectangle( lblGameCompatibilityFix.X, lblGameCompatibilityFix.Bottom + 20, 0, 0); lblMapEditorCompatibilityFix.Text = "FinalSun Compatibility Fix:".L10N("Client:DTAConfig:TSFinalSunFix"); btnMapEditorCompatibilityFix = new XNAClientButton(WindowManager); btnMapEditorCompatibilityFix.Name = "btnMapEditorCompatibilityFix"; btnMapEditorCompatibilityFix.ClientRectangle = new Rectangle( btnGameCompatibilityFix.X, lblMapEditorCompatibilityFix.Y - 4, btnGameCompatibilityFix.Width, btnGameCompatibilityFix.Height); btnMapEditorCompatibilityFix.FontIndex = 1; btnMapEditorCompatibilityFix.Text = "Disable".L10N("Client:DTAConfig:TSDisable"); btnMapEditorCompatibilityFix.LeftClick += BtnMapEditorCompatibilityFix_LeftClick; AddChild(lblGameCompatibilityFix); AddChild(btnGameCompatibilityFix); AddChild(lblMapEditorCompatibilityFix); AddChild(btnMapEditorCompatibilityFix); RegistryKey regKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Tiberian Sun Client"); if (regKey == null) return; object tsCompatFixValue = regKey.GetValue("TSCompatFixInstalled", "No"); string tsCompatFixString = (string)tsCompatFixValue; if (tsCompatFixString == "Yes") { GameCompatFixInstalled = true; } object fsCompatFixValue = regKey.GetValue("FSCompatFixInstalled", "No"); string fsCompatFixString = (string)fsCompatFixValue; if (fsCompatFixString == "Yes") { FinalSunCompatFixInstalled = true; } // These compatibility fixes from 2015 are no longer necessary on modern systems. // They are only offered for uninstallation; if they are not installed, hide them. if (!FinalSunCompatFixInstalled) { lblMapEditorCompatibilityFix.Disable(); btnMapEditorCompatibilityFix.Disable(); } if (!GameCompatFixInstalled) { lblGameCompatibilityFix.Disable(); btnGameCompatibilityFix.Disable(); } if (!FinalSunCompatFixInstalled && !GameCompatFixInstalled) { lblCompatibilityFixes.Disable(); } } /// /// Asks the user whether they want to install the DTA/TI/TS compatibility fix. /// public void PostInit() { Load(); } [SupportedOSPlatform("windows")] private void BtnGameCompatibilityFix_LeftClick(object sender, EventArgs e) { if (GameCompatFixInstalled) { try { Process sdbinst = Process.Start("sdbinst.exe", "-q -n \"TS Compatibility Fix\""); sdbinst.WaitForExit(); Logger.Log("DTA/TI/TS Compatibility Fix succesfully uninstalled."); XNAMessageBox.Show(WindowManager, "Compatibility Fix Uninstalled".L10N("Client:DTAConfig:TSFixUninstallTitle"), "The DTA/TI/TS Compatibility Fix has been succesfully uninstalled.".L10N("Client:DTAConfig:TSFixUninstallText")); RegistryKey regKey = Registry.CurrentUser.OpenSubKey("SOFTWARE", true); regKey = regKey.CreateSubKey("Tiberian Sun Client"); regKey.SetValue("TSCompatFixInstalled", "No"); GameCompatFixInstalled = false; lblGameCompatibilityFix.Disable(); btnGameCompatibilityFix.Disable(); if (!FinalSunCompatFixInstalled) lblCompatibilityFixes.Disable(); } catch (Exception ex) { Logger.Log("Uninstalling DTA/TI/TS Compatibility Fix failed. Error message: " + ex.ToString()); XNAMessageBox.Show(WindowManager, "Uninstalling Compatibility Fix Failed".L10N("Client:DTAConfig:TSFixUninstallFailTitle"), "Uninstalling DTA/TI/TS Compatibility Fix failed. Returned error:".L10N("Client:DTAConfig:TSFixUninstallFailText") + " " + ex.Message); } return; } } [SupportedOSPlatform("windows")] private void BtnMapEditorCompatibilityFix_LeftClick(object sender, EventArgs e) { if (FinalSunCompatFixInstalled) { try { Process sdbinst = Process.Start("sdbinst.exe", "-q -n \"Final Sun Compatibility Fix\""); sdbinst.WaitForExit(); RegistryKey regKey = Registry.CurrentUser.OpenSubKey("SOFTWARE", true); regKey = regKey.CreateSubKey("Tiberian Sun Client"); regKey.SetValue("FSCompatFixInstalled", "No"); btnMapEditorCompatibilityFix.Text = "Enable".L10N("Client:DTAConfig:TSButtonEnable"); Logger.Log("FinalSun Compatibility Fix succesfully uninstalled."); XNAMessageBox.Show(WindowManager, "Compatibility Fix Uninstalled".L10N("Client:DTAConfig:TSFinalSunFixUninstallTitle"), "The FinalSun Compatibility Fix has been succesfully uninstalled.".L10N("Client:DTAConfig:TSFinalSunFixUninstallText")); FinalSunCompatFixInstalled = false; lblMapEditorCompatibilityFix.Disable(); btnMapEditorCompatibilityFix.Disable(); if (!GameCompatFixInstalled) lblCompatibilityFixes.Disable(); } catch (Exception ex) { Logger.Log("Uninstalling FinalSun Compatibility Fix failed. Error message: " + ex.ToString()); XNAMessageBox.Show(WindowManager, "Uninstalling Compatibility Fix Failed".L10N("Client:DTAConfig:TSFinalSunFixUninstallFailedTitle"), "Uninstalling FinalSun Compatibility Fix failed. Error message:".L10N("Client:DTAConfig:TSFinalSunFixUninstallFailedText") + " " + ex.Message); } return; } } private void ChkBorderlessMenu_CheckedChanged(object sender, EventArgs e) { if (chkBorderlessClient.Checked) { ddClientResolution.AllowDropDown = false; string nativeRes = ScreenResolution.SafeFullScreenResolution; int nativeResIndex = ddClientResolution.Items.FindIndex(i => (string)i.Tag == nativeRes); if (nativeResIndex > -1) ddClientResolution.SelectedIndex = nativeResIndex; } else { ddClientResolution.AllowDropDown = true; if (ddClientResolution.PreferredItemIndexes.Count > 0) { // Note: ddClientResolution.PreferredItemIndexes is assumed in ascending order int optimalWindowedResIndex = ddClientResolution.PreferredItemIndexes[^1]; ddClientResolution.SelectedIndex = optimalWindowedResIndex; } } } private void ChkWindowedMode_CheckedChanged(object sender, EventArgs e) { if (chkWindowedMode.Checked) { chkBorderlessWindowedMode.AllowChecking = true; return; } chkBorderlessWindowedMode.AllowChecking = false; chkBorderlessWindowedMode.Checked = false; } /// /// Loads the user's preferred renderer. /// private void LoadRenderer() { int index = ddRenderer.Items.FindIndex( r => ((DirectDrawWrapper)r.Tag).InternalName == directDrawWrapperManager.SelectedRenderer.InternalName); if (index < 0 && directDrawWrapperManager.SelectedRenderer.Hidden) { ddRenderer.AddItem(new XNADropDownItem() { Text = directDrawWrapperManager.SelectedRenderer.UIName, Tag = directDrawWrapperManager.SelectedRenderer }); index = ddRenderer.Items.Count - 1; } ddRenderer.SelectedIndex = index; } public override void Load() { base.Load(); LoadRenderer(); ddDetailLevel.SelectedIndex = UserINISettings.Instance.DetailLevel; string currentRes = UserINISettings.Instance.IngameScreenWidth.Value + "x" + UserINISettings.Instance.IngameScreenHeight.Value; int index = ddIngameResolution.Items.FindIndex(i => i.Text == currentRes); ddIngameResolution.SelectedIndex = index > -1 ? index : 0; // Wonder what this "Win8CompatMode" actually does.. // Disabling it used to be TS-DDRAW only, but it was never enabled after // you had tried TS-DDRAW once, so most players probably have it always // disabled anyway IniSettings.Win8CompatMode.Value = "No"; var renderer = (DirectDrawWrapper)ddRenderer.SelectedItem.Tag; if (renderer.UsesCustomWindowedOption()) { // For renderers that have their own windowed mode implementation // enabled through their own config INI file // (for example DxWnd and CnC-DDRAW) IniFile rendererSettingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, renderer.ConfigFileName)); chkWindowedMode.Checked = rendererSettingsIni.GetBooleanValue(renderer.WindowedModeSection, renderer.WindowedModeKey, false); if (!string.IsNullOrEmpty(renderer.BorderlessWindowedModeKey)) { bool setting = rendererSettingsIni.GetBooleanValue(renderer.WindowedModeSection, renderer.BorderlessWindowedModeKey, false); chkBorderlessWindowedMode.Checked = renderer.IsBorderlessWindowedModeKeyReversed ? !setting : setting; } else { chkBorderlessWindowedMode.Checked = UserINISettings.Instance.BorderlessWindowedMode; } } else { chkWindowedMode.Checked = UserINISettings.Instance.WindowedMode; chkBorderlessWindowedMode.Checked = UserINISettings.Instance.BorderlessWindowedMode; } string currentClientRes = IniSettings.ClientResolutionX.Value + "x" + IniSettings.ClientResolutionY.Value; int clientResIndex = ddClientResolution.Items.FindIndex(i => (string)i.Tag == currentClientRes); ddClientResolution.SelectedIndex = clientResIndex > -1 ? clientResIndex : 0; chkBorderlessClient.Checked = UserINISettings.Instance.BorderlessWindowedClient; int selectedThemeIndex = ddClientTheme.Items.FindIndex( ddi => (string)ddi.Tag == UserINISettings.Instance.ClientTheme); ddClientTheme.SelectedIndex = selectedThemeIndex > -1 ? selectedThemeIndex : 0; foreach (string localeCode in new string[] { UserINISettings.Instance.Translation, Translation.GetDefaultTranslationLocaleCode(), ProgramConstants.HARDCODED_LOCALE_CODE }) { int selectedTranslationIndex = ddTranslation.Items.FindIndex( ddi => localeCode.Equals((string)ddi.Tag, StringComparison.InvariantCultureIgnoreCase)); if (selectedTranslationIndex > -1) { ddTranslation.SelectedIndex = selectedTranslationIndex; break; } } Debug.Assert(ddTranslation.SelectedIndex > -1, "No translation was selected"); if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) { chkBackBufferInVRAM.Checked = !UserINISettings.Instance.BackBufferInVRAM; } else { chkBackBufferInVRAM.Checked = UserINISettings.Instance.BackBufferInVRAM; } } public override bool Save() { bool restartRequired = base.Save(); IniSettings.DetailLevel.Value = ddDetailLevel.SelectedIndex; ScreenResolution ingameRes = ddIngameResolution.SelectedItem.Text; (IniSettings.IngameScreenWidth.Value, IniSettings.IngameScreenHeight.Value) = ingameRes; // Calculate drag selection distance, scale it with resolution width // CustomDragDistance > 0 overrides auto-scaling for players who need a specific value int dragDistance = IniSettings.CustomDragDistance.Value > 0 ? IniSettings.CustomDragDistance.Value : ingameRes.Width / ORIGINAL_RESOLUTION_WIDTH * DRAG_DISTANCE_DEFAULT; IniSettings.DragDistance.Value = dragDistance; var newSelectedRenderer = (DirectDrawWrapper)ddRenderer.SelectedItem.Tag; bool isChangingRenderer = newSelectedRenderer != directDrawWrapperManager.SelectedRenderer; IniSettings.WindowedMode.Value = chkWindowedMode.Checked && !newSelectedRenderer.UsesCustomWindowedOption(); IniSettings.BorderlessWindowedMode.Value = chkBorderlessWindowedMode.Checked && string.IsNullOrEmpty(newSelectedRenderer.BorderlessWindowedModeKey); ScreenResolution clientRes = (string)ddClientResolution.SelectedItem.Tag; if (clientRes.Width != IniSettings.ClientResolutionX.Value || clientRes.Height != IniSettings.ClientResolutionY.Value) restartRequired = true; // TODO: since DTAConfig must not rely on DXMainClient, we can't notify the client to dynamically change the resolution or togging borderless windowed mode. Thus, we need to restart the client as a workaround. (IniSettings.ClientResolutionX.Value, IniSettings.ClientResolutionY.Value) = clientRes; if (IniSettings.BorderlessWindowedClient.Value != chkBorderlessClient.Checked) restartRequired = true; IniSettings.BorderlessWindowedClient.Value = chkBorderlessClient.Checked; if (IniSettings.IntegerScaledClient.Value != chkIntegerScaledClient.Checked) restartRequired = true; IniSettings.IntegerScaledClient.Value = chkIntegerScaledClient.Checked; restartRequired = restartRequired || IniSettings.ClientTheme != (string)ddClientTheme.SelectedItem.Tag; IniSettings.ClientTheme.Value = (string)ddClientTheme.SelectedItem.Tag; { bool updateTranslation = !IniSettings.Translation.ToString().Equals((string)ddTranslation.SelectedItem.Tag, StringComparison.InvariantCultureIgnoreCase); restartRequired = restartRequired || updateTranslation; IniSettings.Translation.Value = (string)ddTranslation.SelectedItem.Tag; if (updateTranslation) IniSettings.TranslationGameFilesVersion.Value = string.Empty; } if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) IniSettings.BackBufferInVRAM.Value = !chkBackBufferInVRAM.Checked; else IniSettings.BackBufferInVRAM.Value = chkBackBufferInVRAM.Checked; directDrawWrapperManager.Save(newSelectedRenderer); if (directDrawWrapperManager.SelectedRenderer.UsesCustomWindowedOption()) { IniFile rendererSettingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, directDrawWrapperManager.SelectedRenderer.ConfigFileName)); rendererSettingsIni.SetBooleanValue(directDrawWrapperManager.SelectedRenderer.WindowedModeSection, directDrawWrapperManager.SelectedRenderer.WindowedModeKey, chkWindowedMode.Checked); if (!string.IsNullOrEmpty(directDrawWrapperManager.SelectedRenderer.BorderlessWindowedModeKey)) { bool borderlessModeIniValue = chkBorderlessWindowedMode.Checked; if (directDrawWrapperManager.SelectedRenderer.IsBorderlessWindowedModeKeyReversed) borderlessModeIniValue = !borderlessModeIniValue; rendererSettingsIni.SetBooleanValue(directDrawWrapperManager.SelectedRenderer.WindowedModeSection, directDrawWrapperManager.SelectedRenderer.BorderlessWindowedModeKey, borderlessModeIniValue); } rendererSettingsIni.WriteIniFile(); } if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) { if (ClientConfiguration.Instance.CopyResolutionDependentLanguageDLL) { string languageDllDestinationPath = SafePath.CombineFilePath(ProgramConstants.GamePath, "Language.dll"); FileInfo fileInfo = SafePath.GetFile(languageDllDestinationPath); if (fileInfo.Exists) { fileInfo.IsReadOnly = false; fileInfo.Delete(); } if (ingameRes.Width >= 1024 && ingameRes.Height >= 720) File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "Resources", "language_1024x720.dll"), languageDllDestinationPath); else if (ingameRes.Width >= 800 && ingameRes.Height >= 600) File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "Resources", "language_800x600.dll"), languageDllDestinationPath); else File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "Resources", "language_640x480.dll"), languageDllDestinationPath); } } #if ISWINDOWS // Since `CheckAndPromptFix` method might restart the client if the admin rights are required, we do this at the end of the Save() method if (isChangingRenderer && !directDrawWrapperManager.SelectedRenderer.IsDummy) DirectDrawCompatibilityChecker.CheckAndPromptFix(WindowManager); #endif return restartRequired; } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionPanels/GameOptionsPanel.cs ================================================ using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using ClientGUI; using ClientCore.Extensions; using ClientCore.Enums; using ClientGUI.Settings; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Generic.OptionPanels { class GameOptionsPanel : XNAOptionsPanel { private const string TEXT_BACKGROUND_COLOR_TRANSPARENT = "0"; private const string TEXT_BACKGROUND_COLOR_BLACK = "12"; private const int MAX_SCROLL_RATE = 6; public GameOptionsPanel(WindowManager windowManager, UserINISettings iniSettings, XNAControl topBar) : base(windowManager, iniSettings) { this.topBar = topBar; } private XNALabel lblScrollRateValue; private XNATrackbar trbScrollRate; private XNAClientCheckBox chkTargetLines; private XNAClientCheckBox chkScrollCoasting; private XNAClientCheckBox chkTooltips; private XNAClientCheckBox chkAltToUndeploy; private XNAClientCheckBox chkBlackChatBackground; private XNAClientCheckBox chkShowHiddenObjects; private XNAControl topBar; private XNATextBox tbPlayerName; private HotkeyConfigurationWindow hotkeyConfigWindow; public override void Initialize() { base.Initialize(); Name = "GameOptionsPanel"; var lblScrollRate = new XNALabel(WindowManager); lblScrollRate.Name = nameof(lblScrollRate); lblScrollRate.ClientRectangle = new Rectangle(12, 14, 0, 0); lblScrollRate.Text = "Scroll Rate:".L10N("Client:DTAConfig:ScrollRate"); lblScrollRateValue = new XNALabel(WindowManager); lblScrollRateValue.Name = nameof(lblScrollRateValue); lblScrollRateValue.FontIndex = 1; lblScrollRateValue.Text = "0"; lblScrollRateValue.ClientRectangle = new Rectangle( Width - lblScrollRateValue.Width - 12, lblScrollRate.Y, 0, 0); trbScrollRate = new XNATrackbar(WindowManager); trbScrollRate.Name = nameof(trbScrollRate); trbScrollRate.ClientRectangle = new Rectangle( lblScrollRate.Right + 32, lblScrollRate.Y - 2, lblScrollRateValue.X - lblScrollRate.Right - 47, 22); trbScrollRate.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2); trbScrollRate.MinValue = 0; trbScrollRate.MaxValue = MAX_SCROLL_RATE; trbScrollRate.ValueChanged += TrbScrollRate_ValueChanged; chkScrollCoasting = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, "ScrollMethod", true, "0", "1"); chkScrollCoasting.Name = nameof(chkScrollCoasting); chkScrollCoasting.ClientRectangle = new Rectangle( lblScrollRate.X, trbScrollRate.Bottom + 20, 0, 0); chkScrollCoasting.Text = "Scroll Coasting".L10N("Client:DTAConfig:ScrollCoasting"); chkTargetLines = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, "UnitActionLines"); chkTargetLines.Name = nameof(chkTargetLines); chkTargetLines.ClientRectangle = new Rectangle( lblScrollRate.X, chkScrollCoasting.Bottom + 24, 0, 0); chkTargetLines.Text = "Target Lines".L10N("Client:DTAConfig:TargetLines"); chkTooltips = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, "ToolTips"); chkTooltips.Name = nameof(chkTooltips); chkTooltips.Text = "Tooltips".L10N("Client:DTAConfig:Tooltips"); var lblPlayerName = new XNALabel(WindowManager); lblPlayerName.Name = nameof(lblPlayerName); lblPlayerName.Text = "Player Name*:".L10N("Client:DTAConfig:PlayerName"); if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) { chkTooltips.ClientRectangle = new Rectangle( lblScrollRate.X, chkTargetLines.Bottom + 24, 0, 0); } else { chkShowHiddenObjects = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, "ShowHidden"); chkShowHiddenObjects.Name = nameof(chkShowHiddenObjects); chkShowHiddenObjects.ClientRectangle = new Rectangle( lblScrollRate.X, chkTargetLines.Bottom + 24, 0, 0); chkShowHiddenObjects.Text = "Show Hidden Objects".L10N("Client:DTAConfig:YRShowHidden"); chkTooltips.ClientRectangle = new Rectangle( lblScrollRate.X, chkShowHiddenObjects.Bottom + 24, 0, 0); lblPlayerName.ClientRectangle = new Rectangle( lblScrollRate.X, chkTooltips.Bottom + 30, 0, 0); AddChild(chkShowHiddenObjects); } if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) { chkBlackChatBackground = new SettingCheckBox(WindowManager, false, UserINISettings.OPTIONS, "TextBackgroundColor", true, TEXT_BACKGROUND_COLOR_BLACK, TEXT_BACKGROUND_COLOR_TRANSPARENT); chkBlackChatBackground.Name = nameof(chkBlackChatBackground); chkBlackChatBackground.ClientRectangle = new Rectangle( chkScrollCoasting.X, chkTooltips.Bottom + 24, 0, 0); chkBlackChatBackground.Text = "Use black background for in-game chat messages".L10N("Client:DTAConfig:TSUseBlackBackgroundChat"); AddChild(chkBlackChatBackground); chkAltToUndeploy = new SettingCheckBox(WindowManager, true, UserINISettings.OPTIONS, "MoveToUndeploy"); chkAltToUndeploy.Name = nameof(chkAltToUndeploy); chkAltToUndeploy.ClientRectangle = new Rectangle( chkScrollCoasting.X, chkBlackChatBackground.Bottom + 24, 0, 0); chkAltToUndeploy.Text = "Undeploy units by holding Alt key instead of a regular move command".L10N("Client:DTAConfig:TSUndeployAltKey"); AddChild(chkAltToUndeploy); lblPlayerName.ClientRectangle = new Rectangle( lblScrollRate.X, chkAltToUndeploy.Bottom + 30, 0, 0); } tbPlayerName = new XNATextBox(WindowManager); tbPlayerName.Name = nameof(tbPlayerName); tbPlayerName.MaximumTextLength = ClientConfiguration.Instance.MaxNameLength; tbPlayerName.ClientRectangle = new Rectangle(trbScrollRate.X, lblPlayerName.Y - 2, 200, 19); tbPlayerName.Text = ProgramConstants.PLAYERNAME; var lblNotice = new XNALabel(WindowManager); lblNotice.Name = nameof(lblNotice); lblNotice.ClientRectangle = new Rectangle(lblPlayerName.X, lblPlayerName.Bottom + 30, 0, 0); lblNotice.Text = ("* If you are currently connected to CnCNet, you need to log out and reconnect\nfor your new name to be applied.").L10N("Client:DTAConfig:ReconnectAfterRename"); hotkeyConfigWindow = new HotkeyConfigurationWindow(WindowManager); DarkeningPanel.AddAndInitializeWithControl(WindowManager, hotkeyConfigWindow); hotkeyConfigWindow.Disable(); var btnConfigureHotkeys = new XNAClientButton(WindowManager); btnConfigureHotkeys.Name = nameof(btnConfigureHotkeys); btnConfigureHotkeys.ClientRectangle = new Rectangle(lblPlayerName.X, lblNotice.Bottom + 36, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT); btnConfigureHotkeys.Text = "Configure Hotkeys".L10N("Client:DTAConfig:ConfigureHotkeys"); btnConfigureHotkeys.LeftClick += BtnConfigureHotkeys_LeftClick; AddChild(lblScrollRate); AddChild(lblScrollRateValue); AddChild(trbScrollRate); AddChild(chkScrollCoasting); AddChild(chkTargetLines); AddChild(chkTooltips); AddChild(lblPlayerName); AddChild(tbPlayerName); AddChild(lblNotice); AddChild(btnConfigureHotkeys); } private void BtnConfigureHotkeys_LeftClick(object sender, EventArgs e) { hotkeyConfigWindow.Enable(); if (topBar.Enabled) { topBar.Disable(); hotkeyConfigWindow.EnabledChanged += HotkeyConfigWindow_EnabledChanged; } } private void HotkeyConfigWindow_EnabledChanged(object sender, EventArgs e) { hotkeyConfigWindow.EnabledChanged -= HotkeyConfigWindow_EnabledChanged; topBar.Enable(); } private void TrbScrollRate_ValueChanged(object sender, EventArgs e) { lblScrollRateValue.Text = trbScrollRate.Value.ToString(); } public override void Load() { base.Load(); int scrollRate = ReverseScrollRate(IniSettings.ScrollRate); if (scrollRate >= trbScrollRate.MinValue && scrollRate <= trbScrollRate.MaxValue) { trbScrollRate.Value = scrollRate; lblScrollRateValue.Text = scrollRate.ToString(); } tbPlayerName.Text = UserINISettings.Instance.PlayerName; } public override bool Save() { bool restartRequired = base.Save(); IniSettings.ScrollRate.Value = ReverseScrollRate(trbScrollRate.Value); string playerName = NameValidator.GetValidOfflineName(tbPlayerName.Text); if (playerName.Length > 0) IniSettings.PlayerName.Value = playerName; return restartRequired; } private int ReverseScrollRate(int scrollRate) { return Math.Abs(scrollRate - MAX_SCROLL_RATE); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionPanels/UpdaterOptionsPanel.cs ================================================ using ClientCore.Extensions; using ClientCore; using ClientGUI; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using ClientUpdater; namespace DTAClient.DXGUI.Generic.OptionPanels { class UpdaterOptionsPanel : XNAOptionsPanel { public UpdaterOptionsPanel(WindowManager windowManager, UserINISettings iniSettings) : base(windowManager, iniSettings) { } public event EventHandler OnForceUpdate; private XNAListBox lbUpdateServerList; private XNAClientCheckBox chkAutoCheck; private XNAClientButton btnForceUpdate; public override void Initialize() { base.Initialize(); Name = "UpdaterOptionsPanel"; var lblDescription = new XNALabel(WindowManager); lblDescription.Name = "lblDescription"; lblDescription.ClientRectangle = new Rectangle(12, 12, 0, 0); lblDescription.Text = ("To change download server priority, select a server from the list and\nuse the Move Up / Down buttons to change its priority.").L10N("Client:DTAConfig:ServerPriorityTip"); lbUpdateServerList = new XNAListBox(WindowManager); lbUpdateServerList.Name = "lblUpdateServerList"; lbUpdateServerList.ClientRectangle = new Rectangle(lblDescription.X, lblDescription.Bottom + 12, Width - 24, 100); lbUpdateServerList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 2, 2); lbUpdateServerList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; var btnMoveUp = new XNAClientButton(WindowManager); btnMoveUp.Name = "btnMoveUp"; btnMoveUp.ClientRectangle = new Rectangle(lbUpdateServerList.X, lbUpdateServerList.Bottom + 12, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnMoveUp.Text = "Move Up".L10N("Client:DTAConfig:MoveUp"); btnMoveUp.LeftClick += btnMoveUp_LeftClick; var btnMoveDown = new XNAClientButton(WindowManager); btnMoveDown.Name = "btnMoveDown"; btnMoveDown.ClientRectangle = new Rectangle( lbUpdateServerList.Right - UIDesignConstants.BUTTON_WIDTH_133, btnMoveUp.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnMoveDown.Text = "Move Down".L10N("Client:DTAConfig:MoveDown"); btnMoveDown.LeftClick += btnMoveDown_LeftClick; chkAutoCheck = new XNAClientCheckBox(WindowManager); chkAutoCheck.Name = "chkAutoCheck"; chkAutoCheck.ClientRectangle = new Rectangle(lblDescription.X, btnMoveUp.Bottom + 24, 0, 0); chkAutoCheck.Text = "Check for updates automatically".L10N("Client:DTAConfig:AutoCheckUpdate"); btnForceUpdate = new XNAClientButton(WindowManager); btnForceUpdate.Name = "btnForceUpdate"; btnForceUpdate.ClientRectangle = new Rectangle(btnMoveDown.X, btnMoveDown.Bottom + 24, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnForceUpdate.Text = "Force Update".L10N("Client:DTAConfig:ForceUpdate"); btnForceUpdate.LeftClick += BtnForceUpdate_LeftClick; AddChild(lblDescription); AddChild(lbUpdateServerList); AddChild(btnMoveUp); AddChild(btnMoveDown); AddChild(chkAutoCheck); AddChild(btnForceUpdate); } private void BtnForceUpdate_LeftClick(object sender, EventArgs e) { var msgBox = new XNAMessageBox(WindowManager, "Force Update Confirmation".L10N("Client:DTAConfig:ForceUpdateConfirmTitle"), ("WARNING: Force update will result in files being re-verified\n" + "and re-downloaded. While this may fix problems with game\n" + "files, this also may delete some custom modifications\n" + "made to this installation. Use at your own risk!\n\n" + "If you proceed, the options window will close and the\n" + "client will proceed to checking for updates.\n\n" + "Do you really want to force update?").L10N("Client:DTAConfig:ForceUpdateConfirmText") + "\n", XNAMessageBoxButtons.YesNo); msgBox.Show(); msgBox.YesClickedAction = ForceUpdateMsgBox_YesClicked; } private void ForceUpdateMsgBox_YesClicked(XNAMessageBox obj) { Updater.ClearVersionInfo(); OnForceUpdate?.Invoke(this, EventArgs.Empty); } private void btnMoveUp_LeftClick(object sender, EventArgs e) { int selectedIndex = lbUpdateServerList.SelectedIndex; if (selectedIndex < 1) return; var tmp = lbUpdateServerList.Items[selectedIndex - 1]; lbUpdateServerList.Items[selectedIndex - 1] = lbUpdateServerList.Items[selectedIndex]; lbUpdateServerList.Items[selectedIndex] = tmp; lbUpdateServerList.SelectedIndex--; Updater.MoveMirrorUp(selectedIndex); } private void btnMoveDown_LeftClick(object sender, EventArgs e) { int selectedIndex = lbUpdateServerList.SelectedIndex; if (selectedIndex > lbUpdateServerList.Items.Count - 2 || selectedIndex < 0) return; var tmp = lbUpdateServerList.Items[selectedIndex + 1]; lbUpdateServerList.Items[selectedIndex + 1] = lbUpdateServerList.Items[selectedIndex]; lbUpdateServerList.Items[selectedIndex] = tmp; lbUpdateServerList.SelectedIndex++; Updater.MoveMirrorDown(selectedIndex); } public override void Load() { base.Load(); lbUpdateServerList.Clear(); foreach (var updaterMirror in Updater.UpdateMirrors) { string name = updaterMirror.Name.L10N($"INI:UpdateMirrors:{updaterMirror.Name}:Name"); string location = updaterMirror.Location.L10N($"INI:UpdateMirrors:{updaterMirror.Name}:Location"); lbUpdateServerList.AddItem(name + (!string.IsNullOrEmpty(location) ? $" ({location})" : string.Empty)); } chkAutoCheck.Checked = IniSettings.CheckForUpdates; } public override bool Save() { bool restartRequired = base.Save(); IniSettings.CheckForUpdates.Value = chkAutoCheck.Checked; IniSettings.SettingsIni.EraseSectionKeys("DownloadMirrors"); int id = 0; foreach (UpdateMirror um in Updater.UpdateMirrors) { IniSettings.SettingsIni.SetStringValue("DownloadMirrors", id.ToString(), um.Name); id++; } return restartRequired; } public override void ToggleMainMenuOnlyOptions(bool enable) { btnForceUpdate.AllowClick = enable; } } } ================================================ FILE: DXMainClient/DXGUI/Generic/OptionsWindow.cs ================================================ using ClientCore.Extensions; using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Enums; using ClientGUI; using DTAClient.DXGUI.Generic.OptionPanels; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using ClientUpdater; using DTAClient.Domain; namespace DTAClient.DXGUI.Generic { public class OptionsWindow : XNAWindow { public OptionsWindow(WindowManager windowManager, GameCollection gameCollection, DirectDrawWrapperManager directDrawWrapperManager) : base(windowManager) { this.gameCollection = gameCollection; this.directDrawWrapperManager = directDrawWrapperManager; } public event EventHandler OnForceUpdate; private XNAClientTabControl tabControl; private XNAOptionsPanel[] optionsPanels; private ComponentsPanel componentsPanel; private DisplayOptionsPanel displayOptionsPanel; private XNAControl topBar; private readonly GameCollection gameCollection; private readonly DirectDrawWrapperManager directDrawWrapperManager; public override void Initialize() { Name = "OptionsWindow"; ClientRectangle = new Rectangle(0, 0, 576, 475); BackgroundTexture = AssetLoader.LoadTextureUncached("optionsbg.png"); tabControl = new XNAClientTabControl(WindowManager); tabControl.Name = "tabControl"; tabControl.ClientRectangle = new Rectangle(12, 12, 0, 23); tabControl.FontIndex = 1; tabControl.ClickSound = new EnhancedSoundEffect("button.wav"); tabControl.AddTab("Display".L10N("Client:DTAConfig:TabDisplay"), UIDesignConstants.BUTTON_WIDTH_92); tabControl.AddTab("Audio".L10N("Client:DTAConfig:TabAudio"), UIDesignConstants.BUTTON_WIDTH_92); tabControl.AddTab("Game".L10N("Client:DTAConfig:TabGame"), UIDesignConstants.BUTTON_WIDTH_92); tabControl.AddTab("CnCNet".L10N("Client:DTAConfig:TabCnCNet"), UIDesignConstants.BUTTON_WIDTH_92); tabControl.AddTab("Updater".L10N("Client:DTAConfig:TabUpdater"), UIDesignConstants.BUTTON_WIDTH_92); tabControl.AddTab("Components".L10N("Client:DTAConfig:TabComponents"), UIDesignConstants.BUTTON_WIDTH_92); tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged; var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = "btnCancel"; btnCancel.ClientRectangle = new Rectangle(Width - 104, Height - 35, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:DTAConfig:ButtonCancel"); btnCancel.LeftClick += BtnBack_LeftClick; var btnSave = new XNAClientButton(WindowManager); btnSave.Name = "btnSave"; btnSave.ClientRectangle = new Rectangle(12, btnCancel.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnSave.Text = "Save".L10N("Client:DTAConfig:ButtonSave"); btnSave.LeftClick += BtnSave_LeftClick; displayOptionsPanel = new DisplayOptionsPanel(WindowManager, UserINISettings.Instance, directDrawWrapperManager); componentsPanel = new ComponentsPanel(WindowManager, UserINISettings.Instance); var updaterOptionsPanel = new UpdaterOptionsPanel(WindowManager, UserINISettings.Instance); updaterOptionsPanel.OnForceUpdate += (s, e) => { Disable(); OnForceUpdate?.Invoke(this, EventArgs.Empty); }; optionsPanels = new XNAOptionsPanel[] { displayOptionsPanel, new AudioOptionsPanel(WindowManager, UserINISettings.Instance), new GameOptionsPanel(WindowManager, UserINISettings.Instance, topBar), new CnCNetOptionsPanel(WindowManager, UserINISettings.Instance, gameCollection), updaterOptionsPanel, componentsPanel }; if (ClientConfiguration.Instance.ModMode || Updater.UpdateMirrors == null || Updater.UpdateMirrors.Count < 1) { tabControl.MakeUnselectable(4); tabControl.MakeUnselectable(5); } else if (Updater.CustomComponents == null || Updater.CustomComponents.Count < 1) tabControl.MakeUnselectable(5); foreach (var panel in optionsPanels) { AddChild(panel); panel.Load(); panel.Disable(); } optionsPanels[0].Enable(); AddChild(tabControl); AddChild(btnCancel); AddChild(btnSave); base.Initialize(); CenterOnParent(); } public void SetTopBar(XNAControl topBar) => this.topBar = topBar; /// /// Parses extra options defined by the modder /// from an INI file. Called from XNAWindow.SetAttributesFromINI. /// /// The INI file. protected override void GetINIAttributes(IniFile iniFile) { base.GetINIAttributes(iniFile); foreach (var panel in optionsPanels) panel.ParseUserOptions(iniFile); } private void TabControl_SelectedIndexChanged(object sender, EventArgs e) { foreach (var panel in optionsPanels) panel.Disable(); optionsPanels[tabControl.SelectedTab].Enable(); optionsPanels[tabControl.SelectedTab].RefreshPanel(); } private void BtnBack_LeftClick(object sender, EventArgs e) { if (Updater.IsComponentDownloadInProgress()) { var msgBox = new XNAMessageBox(WindowManager, "Downloads in progress".L10N("Client:DTAConfig:DownloadingTitle"), ("Optional component downloads are in progress. The downloads will be cancelled if you exit the Options menu.\n\n" + "Are you sure you want to continue?").L10N("Client:DTAConfig:DownloadingText"), XNAMessageBoxButtons.YesNo); msgBox.Show(); msgBox.YesClickedAction = ExitDownloadCancelConfirmation_YesClicked; return; } WindowManager.SoundPlayer.SetVolume(Convert.ToSingle(UserINISettings.Instance.ClientVolume)); Disable(); } private void ExitDownloadCancelConfirmation_YesClicked(XNAMessageBox messageBox) { componentsPanel.CancelAllDownloads(); WindowManager.SoundPlayer.SetVolume(Convert.ToSingle(UserINISettings.Instance.ClientVolume)); Disable(); } private void BtnSave_LeftClick(object sender, EventArgs e) { if (Updater.IsComponentDownloadInProgress()) { XNAMessageBox msgBox = new XNAMessageBox(WindowManager, "Downloads in progress".L10N("Client:DTAConfig:DownloadingTitle"), ("Optional component downloads are in progress. The downloads will be cancelled if you exit the Options menu.\n\n" + "Are you sure you want to continue?").L10N("Client:DTAConfig:DownloadingText"), XNAMessageBoxButtons.YesNo); msgBox.Show(); msgBox.YesClickedAction = SaveDownloadCancelConfirmation_YesClicked; return; } SaveSettings(); } private void SaveDownloadCancelConfirmation_YesClicked(XNAMessageBox messageBox) { componentsPanel.CancelAllDownloads(); SaveSettings(); } private void SaveSettings() { if (RefreshOptionPanels()) return; bool restartRequired = false; try { foreach (var panel in optionsPanels) restartRequired = panel.Save() || restartRequired; UserINISettings.Instance.SaveSettings(); } catch (Exception ex) { Logger.Log("Saving settings failed! Error message: " + ex.ToString()); XNAMessageBox.Show(WindowManager, "Saving Settings Failed".L10N("Client:DTAConfig:SaveSettingFailTitle"), "Saving settings failed! Error message:".L10N("Client:DTAConfig:SaveSettingFailText") + " " + ex.Message); } Disable(); if (restartRequired) { var msgBox = new XNAMessageBox(WindowManager, "Restart Required".L10N("Client:DTAConfig:RestartClientTitle"), ("The client needs to be restarted for some of the changes to take effect.\n\n" + "Do you want to restart now?").L10N("Client:DTAConfig:RestartClientText"), XNAMessageBoxButtons.YesNo); msgBox.Show(); msgBox.YesClickedAction = RestartMsgBox_YesClicked; } } private void RestartMsgBox_YesClicked(XNAMessageBox messageBox) => WindowManager.RestartGame(); /// /// Refreshes the option panels to account for possible /// changes that could affect theirs functionality. /// Shows the popup to inform the user if needed. /// /// A bool that determines whether the /// settings values were changed. private bool RefreshOptionPanels() { bool optionValuesChanged = false; foreach (var panel in optionsPanels) optionValuesChanged = panel.RefreshPanel() || optionValuesChanged; if (optionValuesChanged) { XNAMessageBox.Show(WindowManager, "Setting Value(s) Changed".L10N("Client:DTAConfig:SettingChangedTitle"), ("One or more setting values are\n" + "no longer available and were changed.\n\n" + "You may want to verify the new setting\n" + "values in client's options window.").L10N("Client:DTAConfig:SettingChangedText")); return true; } return false; } public void RefreshSettings() { foreach (var panel in optionsPanels) panel.Load(); RefreshOptionPanels(); foreach (var panel in optionsPanels) panel.Save(); UserINISettings.Instance.SaveSettings(); } public void Open() { foreach (var panel in optionsPanels) panel.Load(); RefreshOptionPanels(); componentsPanel.Open(); Enable(); } public void ToggleMainMenuOnlyOptions(bool enable) { foreach (var panel in optionsPanels) { panel.ToggleMainMenuOnlyOptions(enable); } } public void SwitchToCustomComponentsPanel() { foreach (var panel in optionsPanels) panel.Disable(); tabControl.SelectedTab = 5; } public void InstallCustomComponent(int id) => componentsPanel.InstallComponent(id); public void PostInit() { if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) displayOptionsPanel.PostInit(); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/PrivacyNotification.cs ================================================ using ClientCore; using ClientGUI; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Generic { /// /// A notification that asks the user to accept the CnCNet privacy policy. /// class PrivacyNotification : XNAWindow { public PrivacyNotification(WindowManager windowManager) : base(windowManager) { // DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; } public override void Initialize() { Name = nameof(PrivacyNotification); Width = WindowManager.RenderResolutionX; var lblDescription = new XNALabel(WindowManager); lblDescription.Name = nameof(lblDescription); lblDescription.X = UIDesignConstants.EMPTY_SPACE_SIDES; lblDescription.Y = UIDesignConstants.EMPTY_SPACE_TOP; lblDescription.Text = Renderer.FixText( "This application makes use of CnCNet web & tunnel server services and is subject to collection of technical & other necessary information through them.".L10N("Client:Main:TOSText"), lblDescription.FontIndex, WindowManager.RenderResolutionX - (UIDesignConstants.EMPTY_SPACE_SIDES * 2)).Text; AddChild(lblDescription); var lblMoreInformation = new XNALabel(WindowManager); lblMoreInformation.Name = nameof(lblMoreInformation); lblMoreInformation.X = lblDescription.X; lblMoreInformation.Y = lblDescription.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN; lblMoreInformation.Text = "More information:".L10N("Client:Main:TOSMoreInfo")+ " "; AddChild(lblMoreInformation); var lblTermsAndConditions = new XNALinkLabel(WindowManager); lblTermsAndConditions.Name = nameof(lblTermsAndConditions); lblTermsAndConditions.X = lblMoreInformation.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; lblTermsAndConditions.Y = lblMoreInformation.Y; lblTermsAndConditions.Text = "https://cncnet.org/terms-and-conditions"; lblTermsAndConditions.LeftClick += (s, e) => ProcessLauncher.StartShellProcess(lblTermsAndConditions.Text); AddChild(lblTermsAndConditions); var lblPrivacyPolicy = new XNALinkLabel(WindowManager); lblPrivacyPolicy.Name = nameof(lblPrivacyPolicy); lblPrivacyPolicy.X = lblTermsAndConditions.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; lblPrivacyPolicy.Y = lblMoreInformation.Y; lblPrivacyPolicy.Text = "https://cncnet.org/privacy-policy"; lblPrivacyPolicy.LeftClick += (s, e) => ProcessLauncher.StartShellProcess(lblPrivacyPolicy.Text); AddChild(lblPrivacyPolicy); var lblExplanation = new XNALabel(WindowManager); lblExplanation.Name = nameof(lblExplanation); lblExplanation.X = UIDesignConstants.EMPTY_SPACE_SIDES; lblExplanation.Y = lblMoreInformation.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 2; lblExplanation.Text = "By using this application you agree to the CnCNet Terms & Conditions as well as the CnCNet Privacy Policy. Privacy-related options can be configured in the client settings.".L10N("Client:Main:TOSExplanation"); lblExplanation.TextColor = UISettings.ActiveSettings.SubtleTextColor; AddChild(lblExplanation); var btnOK = new XNAClientButton(WindowManager); btnOK.Name = nameof(btnOK); btnOK.Width = 75; btnOK.Y = lblExplanation.Y; btnOK.X = WindowManager.RenderResolutionX - btnOK.Width - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; btnOK.Text = "Got it".L10N("Client:Main:TOSButtonOK"); AddChild(btnOK); btnOK.LeftClick += (s, e) => { UserINISettings.Instance.PrivacyPolicyAccepted.Value = true; UserINISettings.Instance.SaveSettings(); // AlphaRate = -0.2f; Disable(); }; Height = btnOK.Bottom + UIDesignConstants.EMPTY_SPACE_BOTTOM; Y = WindowManager.RenderResolutionY - Height; base.Initialize(); } public override void Update(GameTime gameTime) { base.Update(gameTime); if (Alpha <= 0.0) Disable(); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/StatisticsWindow.cs ================================================ using ClientCore; using ClientCore.Statistics; using ClientGUI; using DTAClient.Domain.Multiplayer; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.Linq; namespace DTAClient.DXGUI.Generic { public class StatisticsWindow : XNAWindow { public StatisticsWindow(WindowManager windowManager, MapLoader mapLoader) : base(windowManager) { this.mapLoader = mapLoader; } private XNAPanel panelGameStatistics; private XNAPanel panelTotalStatistics; private XNAClientDropDown cmbGameModeFilter; private XNAClientDropDown cmbGameClassFilter; private XNAClientCheckBox chkIncludeSpectatedGames; private XNAClientTabControl tabControl; // Controls for game statistics private XNAMultiColumnListBox lbGameList; private XNAMultiColumnListBox lbGameStatistics; private Texture2D[] sideTextures; // ***************************** private const int TOTAL_STATS_LOCATION_X1 = 40; private const int TOTAL_STATS_VALUE_LOCATION_X1 = 240; private const int TOTAL_STATS_LOCATION_X2 = 380; private const int TOTAL_STATS_VALUE_LOCATION_X2 = 580; private const int TOTAL_STATS_Y_INCREASE = 45; private const int TOTAL_STATS_FIRST_ITEM_Y = 20; // Controls for total statistics private XNALabel lblGamesStartedValue; private XNALabel lblGamesFinishedValue; private XNALabel lblWinsValue; private XNALabel lblLossesValue; private XNALabel lblWinLossRatioValue; private XNALabel lblAverageGameLengthValue; private XNALabel lblTotalTimePlayedValue; private XNALabel lblAverageEnemyCountValue; private XNALabel lblAverageAllyCountValue; private XNALabel lblTotalKillsValue; private XNALabel lblKillsPerGameValue; private XNALabel lblTotalLossesValue; private XNALabel lblLossesPerGameValue; private XNALabel lblKillLossRatioValue; private XNALabel lblTotalScoreValue; private XNALabel lblAverageEconomyValue; private XNALabel lblFavouriteSideValue; private XNALabel lblAverageAILevelValue; // ***************************** private StatisticsManager sm; private MapLoader mapLoader; private List listedGameIndexes = new List(); private (string Name, string UIName)[] sides; private List mpColors; private bool initialized = false; public override void Initialize() { sm = StatisticsManager.Instance; string strLblEconomy = "ECONOMY".L10N("Client:Main:StatisticEconomy"); string strLblAvgEconomy = "Average economy:".L10N("Client:Main:StatisticEconomyAvg"); if (ClientConfiguration.Instance.UseBuiltStatistic) { strLblEconomy = "BUILT".L10N("Client:Main:StatisticBuildCount"); strLblAvgEconomy = "Avg. number of objects built:".L10N("Client:Main:StatisticBuildCountAvg"); } Name = "StatisticsWindow"; BackgroundTexture = AssetLoader.LoadTexture("scoreviewerbg.png"); ClientRectangle = new Rectangle(0, 0, 700, 521); VisibleChanged += StatisticsWindow_VisibleChanged; tabControl = new XNAClientTabControl(WindowManager); tabControl.Name = nameof(tabControl); tabControl.ClientRectangle = new Rectangle(12, 10, 0, 0); tabControl.ClickSound = new EnhancedSoundEffect("button.wav"); tabControl.FontIndex = 1; tabControl.AddTab("Game Statistics".L10N("Client:Main:GameStatistic"), UIDesignConstants.BUTTON_WIDTH_133); tabControl.AddTab("Total Statistics".L10N("Client:Main:TotalStatistic"), UIDesignConstants.BUTTON_WIDTH_133); tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged; XNALabel lblFilter = new XNALabel(WindowManager); lblFilter.Name = nameof(lblFilter); lblFilter.FontIndex = 1; lblFilter.Text = "FILTER:".L10N("Client:Main:Filter"); lblFilter.ClientRectangle = new Rectangle(527, 12, 0, 0); cmbGameClassFilter = new XNAClientDropDown(WindowManager); cmbGameClassFilter.ClientRectangle = new Rectangle(585, 11, 105, 21); cmbGameClassFilter.Name = nameof(cmbGameClassFilter); cmbGameClassFilter.AddItem("All games".L10N("Client:Main:FilterAll")); cmbGameClassFilter.AddItem("Online games".L10N("Client:Main:FilterOnline")); cmbGameClassFilter.AddItem("Online PvP".L10N("Client:Main:FilterPvP")); cmbGameClassFilter.AddItem("Online Co-Op".L10N("Client:Main:FilterCoOp")); cmbGameClassFilter.AddItem("Skirmish".L10N("Client:Main:FilterSkirmish")); cmbGameClassFilter.SelectedIndex = 0; cmbGameClassFilter.SelectedIndexChanged += CmbGameClassFilter_SelectedIndexChanged; XNALabel lblGameMode = new XNALabel(WindowManager); lblGameMode.Name = nameof(lblGameMode); lblGameMode.FontIndex = 1; lblGameMode.Text = "GAME MODE:".L10N("Client:Main:GameMode"); lblGameMode.ClientRectangle = new Rectangle(294, 12, 0, 0); cmbGameModeFilter = new XNAClientDropDown(WindowManager); cmbGameModeFilter.Name = nameof(cmbGameModeFilter); cmbGameModeFilter.ClientRectangle = new Rectangle(381, 11, 114, 21); cmbGameModeFilter.SelectedIndexChanged += CmbGameModeFilter_SelectedIndexChanged; var btnReturnToMenu = new XNAClientButton(WindowManager); btnReturnToMenu.Name = nameof(btnReturnToMenu); btnReturnToMenu.ClientRectangle = new Rectangle(270, 486, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT); btnReturnToMenu.Text = "Return to Main Menu".L10N("Client:Main:ReturnToMainMenu"); btnReturnToMenu.LeftClick += BtnReturnToMenu_LeftClick; var btnClearStatistics = new XNAClientButton(WindowManager); btnClearStatistics.Name = nameof(btnClearStatistics); btnClearStatistics.ClientRectangle = new Rectangle(12, 486, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT); btnClearStatistics.Text = "Clear Statistics".L10N("Client:Main:ClearStatistics"); btnClearStatistics.LeftClick += BtnClearStatistics_LeftClick; btnClearStatistics.Visible = false; chkIncludeSpectatedGames = new XNAClientCheckBox(WindowManager); AddChild(chkIncludeSpectatedGames); chkIncludeSpectatedGames.Name = nameof(chkIncludeSpectatedGames); chkIncludeSpectatedGames.Text = "Include spectated games".L10N("Client:Main:IncludeSpectated"); chkIncludeSpectatedGames.Checked = true; chkIncludeSpectatedGames.ClientRectangle = new Rectangle( Width - chkIncludeSpectatedGames.Width - 12, cmbGameModeFilter.Bottom + 3, chkIncludeSpectatedGames.Width, chkIncludeSpectatedGames.Height); chkIncludeSpectatedGames.CheckedChanged += ChkIncludeSpectatedGames_CheckedChanged; #region Match statistics panelGameStatistics = new XNAPanel(WindowManager); panelGameStatistics.Name = "panelGameStatistics"; panelGameStatistics.BackgroundTexture = AssetLoader.LoadTexture("scoreviewerpanelbg.png"); panelGameStatistics.ClientRectangle = new Rectangle(10, 55, 680, 425); AddChild(panelGameStatistics); XNALabel lblGames = new XNALabel(WindowManager); lblGames.Name = nameof(lblGames); lblGames.Text = "GAMES:".L10N("Client:Main:GameMatches"); lblGames.FontIndex = 1; lblGames.ClientRectangle = new Rectangle(4, 2, 0, 0); lbGameList = new XNAMultiColumnListBox(WindowManager); lbGameList.Name = nameof(lbGameList); lbGameList.ClientRectangle = new Rectangle(2, 25, 676, 250); lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameList.AddColumn("DATE / TIME".L10N("Client:Main:GameMatchDateTimeColumnHeader"), 130); lbGameList.AddColumn("MAP".L10N("Client:Main:GameMatchMapColumnHeader"), 200); lbGameList.AddColumn("GAME MODE".L10N("Client:Main:GameMatchGameModeColumnHeader"), 130); lbGameList.AddColumn("FPS".L10N("Client:Main:GameMatchFPSColumnHeader"), 50); lbGameList.AddColumn("DURATION".L10N("Client:Main:GameMatchDurationColumnHeader"), 76); lbGameList.AddColumn("COMPLETED".L10N("Client:Main:GameMatchCompletedColumnHeader"), 90); lbGameList.SelectedIndexChanged += LbGameList_SelectedIndexChanged; lbGameList.AllowKeyboardInput = true; lbGameStatistics = new XNAMultiColumnListBox(WindowManager); lbGameStatistics.Name = nameof(lbGameStatistics); lbGameStatistics.ClientRectangle = new Rectangle(2, 280, 676, 143); lbGameStatistics.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbGameStatistics.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameStatistics.AddColumn("NAME".L10N("Client:Main:StatisticsName"), 130); lbGameStatistics.AddColumn("KILLS".L10N("Client:Main:StatisticsKills"), 78); lbGameStatistics.AddColumn("LOSSES".L10N("Client:Main:StatisticsLosses"), 78); lbGameStatistics.AddColumn(strLblEconomy, 80); lbGameStatistics.AddColumn("SCORE".L10N("Client:Main:StatisticsScore"), 100); lbGameStatistics.AddColumn("WON".L10N("Client:Main:StatisticsWon"), 50); lbGameStatistics.AddColumn("SIDE".L10N("Client:Main:StatisticsSide"), 100); lbGameStatistics.AddColumn("TEAM".L10N("Client:Main:StatisticsTeam"), 60); panelGameStatistics.AddChild(lblGames); panelGameStatistics.AddChild(lbGameList); panelGameStatistics.AddChild(lbGameStatistics); #endregion #region Total statistics panelTotalStatistics = new XNAPanel(WindowManager); panelTotalStatistics.Name = "panelTotalStatistics"; panelTotalStatistics.BackgroundTexture = AssetLoader.LoadTexture("scoreviewerpanelbg.png"); panelTotalStatistics.ClientRectangle = new Rectangle(10, 55, 680, 425); AddChild(panelTotalStatistics); panelTotalStatistics.Visible = false; panelTotalStatistics.Enabled = false; int locationY = TOTAL_STATS_FIRST_ITEM_Y; AddTotalStatisticsLabel("lblGamesStarted", "Games started:".L10N("Client:Main:StatisticsGamesStarted"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblGamesStartedValue = new XNALabel(WindowManager); lblGamesStartedValue.Name = "lblGamesStartedValue"; lblGamesStartedValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblGamesStartedValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblGamesFinished", "Games finished:".L10N("Client:Main:StatisticsGamesFinished"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblGamesFinishedValue = new XNALabel(WindowManager); lblGamesFinishedValue.Name = "lblGamesFinishedValue"; lblGamesFinishedValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblGamesFinishedValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblWins", "Wins:".L10N("Client:Main:StatisticsGamesWins"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblWinsValue = new XNALabel(WindowManager); lblWinsValue.Name = "lblWinsValue"; lblWinsValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblWinsValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblLosses", "Losses:".L10N("Client:Main:StatisticsGamesLosses"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblLossesValue = new XNALabel(WindowManager); lblLossesValue.Name = "lblLossesValue"; lblLossesValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblLossesValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblWinLossRatio", "Win / Loss ratio:".L10N("Client:Main:StatisticsGamesWinLossRatio"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblWinLossRatioValue = new XNALabel(WindowManager); lblWinLossRatioValue.Name = "lblWinLossRatioValue"; lblWinLossRatioValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblWinLossRatioValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblAverageGameLength", "Average game length:".L10N("Client:Main:StatisticsGamesLengthAvg"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblAverageGameLengthValue = new XNALabel(WindowManager); lblAverageGameLengthValue.Name = "lblAverageGameLengthValue"; lblAverageGameLengthValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblAverageGameLengthValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblTotalTimePlayed", "Total time played:".L10N("Client:Main:StatisticsTotalTimePlayed"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblTotalTimePlayedValue = new XNALabel(WindowManager); lblTotalTimePlayedValue.Name = "lblTotalTimePlayedValue"; lblTotalTimePlayedValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblTotalTimePlayedValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblAverageEnemyCount", "Average number of enemies:".L10N("Client:Main:StatisticsEnemiesAvg"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblAverageEnemyCountValue = new XNALabel(WindowManager); lblAverageEnemyCountValue.Name = "lblAverageEnemyCountValue"; lblAverageEnemyCountValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblAverageEnemyCountValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblAverageAllyCount", "Average number of allies:".L10N("Client:Main:StatisticsAlliesAvg"), new Point(TOTAL_STATS_LOCATION_X1, locationY)); lblAverageAllyCountValue = new XNALabel(WindowManager); lblAverageAllyCountValue.Name = "lblAverageAllyCountValue"; lblAverageAllyCountValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X1, locationY, 0, 0); lblAverageAllyCountValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; // SECOND COLUMN locationY = TOTAL_STATS_FIRST_ITEM_Y; AddTotalStatisticsLabel("lblTotalKills", "Total kills:".L10N("Client:Main:StatisticsTotalKills"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblTotalKillsValue = new XNALabel(WindowManager); lblTotalKillsValue.Name = "lblTotalKillsValue"; lblTotalKillsValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblTotalKillsValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblKillsPerGame", "Kills / game:".L10N("Client:Main:StatisticsKillsPerGame"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblKillsPerGameValue = new XNALabel(WindowManager); lblKillsPerGameValue.Name = "lblKillsPerGameValue"; lblKillsPerGameValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblKillsPerGameValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblTotalLosses", "Total losses:".L10N("Client:Main:StatisticsTotalLosses"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblTotalLossesValue = new XNALabel(WindowManager); lblTotalLossesValue.Name = "lblTotalLossesValue"; lblTotalLossesValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblTotalLossesValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblLossesPerGame", "Losses / game:".L10N("Client:Main:StatisticsLossesPerGame"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblLossesPerGameValue = new XNALabel(WindowManager); lblLossesPerGameValue.Name = "lblLossesPerGameValue"; lblLossesPerGameValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblLossesPerGameValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblKillLossRatio", "Kill / loss ratio:".L10N("Client:Main:StatisticsKillLossRatio"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblKillLossRatioValue = new XNALabel(WindowManager); lblKillLossRatioValue.Name = "lblKillLossRatioValue"; lblKillLossRatioValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblKillLossRatioValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblTotalScore", "Total score:".L10N("Client:Main:TotalScore"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblTotalScoreValue = new XNALabel(WindowManager); lblTotalScoreValue.Name = "lblTotalScoreValue"; lblTotalScoreValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblTotalScoreValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblAverageEconomy", strLblAvgEconomy, new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblAverageEconomyValue = new XNALabel(WindowManager); lblAverageEconomyValue.Name = "lblAverageEconomyValue"; lblAverageEconomyValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblAverageEconomyValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblFavouriteSide", "Favourite side:".L10N("Client:Main:FavouriteSide"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblFavouriteSideValue = new XNALabel(WindowManager); lblFavouriteSideValue.Name = "lblFavouriteSideValue"; lblFavouriteSideValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblFavouriteSideValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; AddTotalStatisticsLabel("lblAverageAILevel", "Average AI level:".L10N("Client:Main:AvgAILevel"), new Point(TOTAL_STATS_LOCATION_X2, locationY)); lblAverageAILevelValue = new XNALabel(WindowManager); lblAverageAILevelValue.Name = "lblAverageAILevelValue"; lblAverageAILevelValue.ClientRectangle = new Rectangle(TOTAL_STATS_VALUE_LOCATION_X2, locationY, 0, 0); lblAverageAILevelValue.RemapColor = UISettings.ActiveSettings.AltColor; locationY += TOTAL_STATS_Y_INCREASE; panelTotalStatistics.AddChild(lblGamesStartedValue); panelTotalStatistics.AddChild(lblGamesFinishedValue); panelTotalStatistics.AddChild(lblWinsValue); panelTotalStatistics.AddChild(lblLossesValue); panelTotalStatistics.AddChild(lblWinLossRatioValue); panelTotalStatistics.AddChild(lblAverageGameLengthValue); panelTotalStatistics.AddChild(lblTotalTimePlayedValue); panelTotalStatistics.AddChild(lblAverageEnemyCountValue); panelTotalStatistics.AddChild(lblAverageAllyCountValue); panelTotalStatistics.AddChild(lblTotalKillsValue); panelTotalStatistics.AddChild(lblKillsPerGameValue); panelTotalStatistics.AddChild(lblTotalLossesValue); panelTotalStatistics.AddChild(lblLossesPerGameValue); panelTotalStatistics.AddChild(lblKillLossRatioValue); panelTotalStatistics.AddChild(lblTotalScoreValue); panelTotalStatistics.AddChild(lblAverageEconomyValue); panelTotalStatistics.AddChild(lblFavouriteSideValue); panelTotalStatistics.AddChild(lblAverageAILevelValue); #endregion AddChild(tabControl); AddChild(lblFilter); AddChild(cmbGameClassFilter); AddChild(lblGameMode); AddChild(cmbGameModeFilter); AddChild(btnReturnToMenu); AddChild(btnClearStatistics); base.Initialize(); CenterOnParent(); sides = ClientConfiguration.Instance.Sides.Split(',') .Select(s => (Name: s, UIName: s.L10N($"INI:Sides:{s}"))).ToArray(); sideTextures = new Texture2D[sides.Length + 1]; for (int i = 0; i < sides.Length; i++) sideTextures[i] = AssetLoader.LoadTexture(sides[i].Name + "icon.png"); sideTextures[sides.Length] = AssetLoader.LoadTexture("spectatoricon.png"); mpColors = MultiplayerColor.LoadColors(); ReadStatistics(); ListGameModes(); StatisticsManager.Instance.GameAdded += Instance_GameAdded; initialized = true; } private void StatisticsWindow_VisibleChanged(object sender, EventArgs e) { ListGames(); } private void Instance_GameAdded(object sender, EventArgs e) { ListGames(); } private void ChkIncludeSpectatedGames_CheckedChanged(object sender, EventArgs e) { ListGames(); } private void AddTotalStatisticsLabel(string name, string text, Point location) { XNALabel label = new XNALabel(WindowManager); label.Name = name; label.Text = text; label.ClientRectangle = new Rectangle(location.X, location.Y, 0, 0); panelTotalStatistics.AddChild(label); } private void TabControl_SelectedIndexChanged(object sender, EventArgs e) { if (tabControl.SelectedTab == 1) { panelGameStatistics.Visible = false; panelGameStatistics.Enabled = false; panelTotalStatistics.Visible = true; panelTotalStatistics.Enabled = true; } else { panelGameStatistics.Visible = true; panelGameStatistics.Enabled = true; panelTotalStatistics.Visible = false; panelTotalStatistics.Enabled = false; } } private void CmbGameClassFilter_SelectedIndexChanged(object sender, EventArgs e) { ListGames(); } private void CmbGameModeFilter_SelectedIndexChanged(object sender, EventArgs e) { ListGames(); } private void LbGameList_SelectedIndexChanged(object sender, EventArgs e) { lbGameStatistics.ClearItems(); if (lbGameList.SelectedIndex == -1) return; MatchStatistics ms = sm.GetMatchByIndex(listedGameIndexes[lbGameList.SelectedIndex]); List players = new List(); for (int i = 0; i < ms.GetPlayerCount(); i++) { players.Add(ms.GetPlayer(i)); } players = players.OrderBy(p => p.Score).Reverse().ToList(); Color textColor = UISettings.ActiveSettings.AltColor; for (int i = 0; i < ms.GetPlayerCount(); i++) { PlayerStatistics ps = players[i]; //List items = new List(); List items = new List(); if (ps.Color > -1 && ps.Color < mpColors.Count) textColor = mpColors[ps.Color].XnaColor; if (ps.IsAI) { items.Add(new XNAListBoxItem(ProgramConstants.GetAILevelName(ps.AILevel), textColor)); } else items.Add(new XNAListBoxItem(ps.Name, textColor)); if (ps.WasSpectator) { // Player was a spectator items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); XNAListBoxItem spectatorItem = new XNAListBoxItem(); spectatorItem.Text = "Spectator".L10N("Client:Main:Spectator"); spectatorItem.TextColor = textColor; spectatorItem.Texture = sideTextures[sideTextures.Length - 1]; items.Add(spectatorItem); items.Add(new XNAListBoxItem("-", textColor)); } else { if (!ms.SawCompletion) { // The game wasn't completed - we don't know the stats items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); items.Add(new XNAListBoxItem("-", textColor)); } else { // The game was completed and the player was actually playing items.Add(new XNAListBoxItem(ps.Kills.ToString(), textColor)); items.Add(new XNAListBoxItem(ps.Losses.ToString(), textColor)); items.Add(new XNAListBoxItem(ps.Economy.ToString(), textColor)); items.Add(new XNAListBoxItem(ps.Score.ToString(), textColor)); items.Add(new XNAListBoxItem( Conversions.BooleanToString(ps.Won, BooleanStringStyle.YESNO), textColor)); } if (ps.Side == 0 || ps.Side > sides.Length) items.Add(new XNAListBoxItem("Unknown".L10N("Client:Main:UnknownSide"), textColor)); else { XNAListBoxItem sideItem = new XNAListBoxItem(); sideItem.Text = sides[ps.Side - 1].UIName; sideItem.TextColor = textColor; sideItem.Texture = sideTextures[ps.Side - 1]; items.Add(sideItem); } items.Add(new XNAListBoxItem(TeamIndexToString(ps.Team), textColor)); } if (!ps.IsLocalPlayer) { lbGameStatistics.AddItem(items); items.ForEach(item => item.Selectable = false); } else { lbGameStatistics.AddItem(items); lbGameStatistics.SelectedIndex = i; } } } private string TeamIndexToString(int teamIndex) { if (teamIndex < 1 || teamIndex >= ProgramConstants.TEAMS.Count) return "-"; return ProgramConstants.TEAMS[teamIndex - 1]; } #region Statistics reading / game listing code private void ReadStatistics() { StatisticsManager sm = StatisticsManager.Instance; sm.ReadStatistics(ProgramConstants.GamePath); } private void ListGameModes() { int gameCount = sm.GetMatchCount(); List gameModes = new List(); cmbGameModeFilter.Items.Clear(); cmbGameModeFilter.AddItem("All".L10N("Client:Main:AllGameModes")); for (int i = 0; i < gameCount; i++) { MatchStatistics ms = sm.GetMatchByIndex(i); if (!gameModes.Contains(ms.GameMode)) gameModes.Add(ms.GameMode); } gameModes.Sort(); foreach (string gm in gameModes) cmbGameModeFilter.AddItem(new XNADropDownItem { Text = gm.L10N($"INI:GameModes:{gm}:UIName"), Tag = gm }); cmbGameModeFilter.SelectedIndex = 0; } private void ListGames() { if (!Visible || !initialized) return; lbGameList.SelectedIndex = -1; lbGameList.SetTopIndex(0); lbGameStatistics.ClearItems(); lbGameList.ClearItems(); listedGameIndexes.Clear(); switch (cmbGameClassFilter.SelectedIndex) { case 0: ListAllGames(); break; case 1: ListOnlineGames(); break; case 2: ListPvPGames(); break; case 3: ListCoOpGames(); break; case 4: ListSkirmishGames(); break; } listedGameIndexes.Reverse(); SetTotalStatistics(); foreach (int gameIndex in listedGameIndexes) { MatchStatistics ms = sm.GetMatchByIndex(gameIndex); string dateTime = ms.DateAndTime.ToShortDateString() + " " + ms.DateAndTime.ToShortTimeString(); List info = new List(); info.Add(Renderer.GetSafeString(dateTime, lbGameList.FontIndex)); info.Add(mapLoader.TranslatedMapNames.ContainsKey(ms.MapName) ? mapLoader.TranslatedMapNames[ms.MapName] : ms.MapName); info.Add(ms.GameMode.L10N($"INI:GameModes:{ms.GameMode}:UIName")); if (ms.AverageFPS == 0) info.Add("-"); else info.Add(ms.AverageFPS.ToString()); info.Add(Renderer.GetSafeString(TimeSpan.FromSeconds(ms.LengthInSeconds).ToString(), lbGameList.FontIndex)); info.Add(Conversions.BooleanToString(ms.SawCompletion, BooleanStringStyle.YESNO)); lbGameList.AddItem(info, true); } } private void ListAllGames() { int gameCount = sm.GetMatchCount(); for (int i = 0; i < gameCount; i++) { ListGameIndexIfPrerequisitesMet(i); } } private void ListOnlineGames() { int gameCount = sm.GetMatchCount(); for (int i = 0; i < gameCount; i++) { MatchStatistics ms = sm.GetMatchByIndex(i); int pCount = ms.GetPlayerCount(); int hpCount = 0; for (int j = 0; j < pCount; j++) { PlayerStatistics ps = ms.GetPlayer(j); if (!ps.IsAI) { hpCount++; if (hpCount > 1) { ListGameIndexIfPrerequisitesMet(i); break; } } } } } private void ListPvPGames() { int gameCount = sm.GetMatchCount(); for (int i = 0; i < gameCount; i++) { MatchStatistics ms = sm.GetMatchByIndex(i); int pCount = ms.GetPlayerCount(); int pTeam = -1; for (int j = 0; j < pCount; j++) { PlayerStatistics ps = ms.GetPlayer(j); if (!ps.IsAI && !ps.WasSpectator) { // If we find a single player on a different team than another player, // we'll count the game as a PvP game if (pTeam > -1 && (ps.Team != pTeam || ps.Team == 0)) { ListGameIndexIfPrerequisitesMet(i); break; } pTeam = ps.Team; } } } } private void ListCoOpGames() { int gameCount = sm.GetMatchCount(); for (int i = 0; i < gameCount; i++) { MatchStatistics ms = sm.GetMatchByIndex(i); int pCount = ms.GetPlayerCount(); int hpCount = 0; int pTeam = -1; bool add = true; for (int j = 0; j < pCount; j++) { PlayerStatistics ps = ms.GetPlayer(j); if (!ps.IsAI && !ps.WasSpectator) { hpCount++; if (pTeam > -1 && (ps.Team != pTeam || ps.Team == 0)) { add = false; break; } pTeam = ps.Team; } } if (add && hpCount > 1) { ListGameIndexIfPrerequisitesMet(i); } } } private void ListSkirmishGames() { int gameCount = sm.GetMatchCount(); for (int i = 0; i < gameCount; i++) { MatchStatistics ms = sm.GetMatchByIndex(i); int pCount = ms.GetPlayerCount(); int hpCount = 0; bool add = true; foreach (PlayerStatistics ps in ms.Players) { if (!ps.IsAI) { hpCount++; if (hpCount > 1) { add = false; break; } } } if (add) { ListGameIndexIfPrerequisitesMet(i); } } } private void ListGameIndexIfPrerequisitesMet(int gameIndex) { MatchStatistics ms = sm.GetMatchByIndex(gameIndex); if (cmbGameModeFilter.SelectedIndex != 0) { // "All" doesn't have a tag but that doesn't matter since 0 is not checked var gameMode = (string)cmbGameModeFilter.Items[cmbGameModeFilter.SelectedIndex].Tag; if (ms.GameMode != gameMode) return; } PlayerStatistics ps = ms.Players.Find(p => p.IsLocalPlayer); if (ps != null && !chkIncludeSpectatedGames.Checked) { if (ps.WasSpectator) return; } listedGameIndexes.Add(gameIndex); } /// /// Adjusts the labels on the "Total statistics" tab. /// private void SetTotalStatistics() { int gamesStarted = 0; int gamesFinished = 0; int gamesPlayed = 0; int wins = 0; int gameLosses = 0; TimeSpan timePlayed = TimeSpan.Zero; int numEnemies = 0; int numAllies = 0; int totalKills = 0; int totalLosses = 0; int totalScore = 0; int totalEconomy = 0; int[] sideGameCounts = new int[sides.Length]; int numEasyAIs = 0; int numMediumAIs = 0; int numHardAIs = 0; foreach (int gameIndex in listedGameIndexes) { MatchStatistics ms = sm.GetMatchByIndex(gameIndex); gamesStarted++; if (ms.SawCompletion) gamesFinished++; timePlayed += TimeSpan.FromSeconds(ms.LengthInSeconds); PlayerStatistics localPlayer = FindLocalPlayer(ms); if (!localPlayer.WasSpectator) { totalKills += localPlayer.Kills; totalLosses += localPlayer.Losses; totalScore += localPlayer.Score; totalEconomy += localPlayer.Economy; if (localPlayer.Side > 0 && localPlayer.Side <= sides.Length) sideGameCounts[localPlayer.Side - 1]++; if (!ms.SawCompletion) continue; if (localPlayer.Won) wins++; else gameLosses++; gamesPlayed++; for (int i = 0; i < ms.GetPlayerCount(); i++) { PlayerStatistics ps = ms.GetPlayer(i); if (!ps.WasSpectator && (!ps.IsLocalPlayer || ps.IsAI)) { if (ps.Team == 0 || localPlayer.Team != ps.Team) numEnemies++; else numAllies++; if (ps.IsAI) { if (ps.AILevel == 0) numEasyAIs++; else if (ps.AILevel == 1) numMediumAIs++; else numHardAIs++; } } } } } lblGamesStartedValue.Text = gamesStarted.ToString(); lblGamesFinishedValue.Text = gamesFinished.ToString(); lblWinsValue.Text = wins.ToString(); lblLossesValue.Text = gameLosses.ToString(); if (gameLosses > 0) { lblWinLossRatioValue.Text = Math.Round(wins / (double)gameLosses, 2).ToString(); } else lblWinLossRatioValue.Text = "-"; if (gamesStarted > 0) { lblAverageGameLengthValue.Text = TimeSpan.FromSeconds((int)timePlayed.TotalSeconds / gamesStarted).ToString(); } else lblAverageGameLengthValue.Text = "-"; if (gamesPlayed > 0) { lblAverageEnemyCountValue.Text = Math.Round(numEnemies / (double)gamesPlayed, 2).ToString(); lblAverageAllyCountValue.Text = Math.Round(numAllies / (double)gamesPlayed, 2).ToString(); lblKillsPerGameValue.Text = (totalKills / gamesPlayed).ToString(); lblLossesPerGameValue.Text = (totalLosses / gamesPlayed).ToString(); lblAverageEconomyValue.Text = (totalEconomy / gamesPlayed).ToString(); } else { lblAverageEnemyCountValue.Text = "-"; lblAverageAllyCountValue.Text = "-"; lblKillsPerGameValue.Text = "-"; lblLossesPerGameValue.Text = "-"; lblAverageEconomyValue.Text = "-"; } if (totalLosses > 0) { lblKillLossRatioValue.Text = Math.Round(totalKills / (double)totalLosses, 2).ToString(); } else lblKillLossRatioValue.Text = "-"; lblTotalTimePlayedValue.Text = timePlayed.ToString(); lblTotalKillsValue.Text = totalKills.ToString(); lblTotalLossesValue.Text = totalLosses.ToString(); lblTotalScoreValue.Text = totalScore.ToString(); lblFavouriteSideValue.Text = sides[GetHighestIndex(sideGameCounts)].UIName; if (numEasyAIs >= numMediumAIs && numEasyAIs >= numHardAIs) lblAverageAILevelValue.Text = "Easy".L10N("Client:Main:EasyAI"); else if (numMediumAIs >= numEasyAIs && numMediumAIs >= numHardAIs) lblAverageAILevelValue.Text = "Medium".L10N("Client:Main:MediumAI"); else lblAverageAILevelValue.Text = "Hard".L10N("Client:Main:HardAI"); } private PlayerStatistics FindLocalPlayer(MatchStatistics ms) { int pCount = ms.GetPlayerCount(); for (int pId = 0; pId < pCount; pId++) { PlayerStatistics ps = ms.GetPlayer(pId); if (!ps.IsAI && ps.IsLocalPlayer) return ps; } return null; } private int GetHighestIndex(int[] t) { int highestIndex = -1; int highest = Int32.MinValue; for (int i = 0; i < t.Length; i++) { if (t[i] > highest) { highest = t[i]; highestIndex = i; } } return highestIndex; } private void ClearAllStatistics() { StatisticsManager.Instance.ClearDatabase(); ReadStatistics(); ListGameModes(); ListGames(); } #endregion private void BtnReturnToMenu_LeftClick(object sender, EventArgs e) { Disable(); } private void BtnClearStatistics_LeftClick(object sender, EventArgs e) { var msgBox = new XNAMessageBox(WindowManager, "Clear all statistics".L10N("Client:Main:ClearStatisticsTitle"), ("All statistics data will be cleared from the database.\n\nAre you sure you want to continue?").L10N("Client:Main:ClearStatisticsText"), XNAMessageBoxButtons.YesNo); msgBox.Show(); msgBox.YesClickedAction = ClearStatisticsConfirmation_YesClicked; } private void ClearStatisticsConfirmation_YesClicked(XNAMessageBox messageBox) { ClearAllStatistics(); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/TopBar.cs ================================================ using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using Rampastring.XNAUI; using Microsoft.Xna.Framework; using Rampastring.XNAUI.Input; using Microsoft.Xna.Framework.Input; using DTAClient.Online; using ClientGUI; using ClientCore; using System.Threading; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; using ClientCore.Extensions; namespace DTAClient.DXGUI.Generic { /// /// A top bar that allows switching between various client windows. /// public class TopBar : XNAPanel { /// /// The number of seconds that the top bar will stay down after it has /// lost input focus. /// const double DOWN_TIME_WAIT_SECONDS = 1.0; const double EVENT_DOWN_TIME_WAIT_SECONDS = 2.0; const double STARTUP_DOWN_TIME_WAIT_SECONDS = 3.5; const double DOWN_MOVEMENT_RATE = 1.7; const double UP_MOVEMENT_RATE = 1.7; const int APPEAR_CURSOR_THRESHOLD_Y = 8; private readonly string DEFAULT_PM_BTN_LABEL = "Private Messages (F4)".L10N("Client:Main:PMButtonF4"); public TopBar( WindowManager windowManager, CnCNetManager connectionManager, PrivateMessageHandler privateMessageHandler ) : base(windowManager) { downTimeWaitTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS); this.connectionManager = connectionManager; this.privateMessageHandler = privateMessageHandler; } public SwitchType LastSwitchType { get; private set; } private List primarySwitches = new List(); private ISwitchable cncnetLobbySwitch; private ISwitchable privateMessageSwitch; private OptionsWindow optionsWindow; private XNAClientButton btnMainButton; private XNAClientButton btnCnCNetLobby; private XNAClientButton btnPrivateMessages; private XNAClientButton btnOptions; private XNAClientButton btnLogout; private XNALabel lblTime; private XNALabel lblDate; private XNALabel lblCnCNetStatus; private XNALabel lblCnCNetPlayerCount; private XNALabel lblConnectionStatus; private CnCNetManager connectionManager; private readonly PrivateMessageHandler privateMessageHandler; private CancellationTokenSource cncnetPlayerCountCancellationSource; private static readonly object locker = new object(); private TimeSpan downTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS - STARTUP_DOWN_TIME_WAIT_SECONDS); private TimeSpan downTimeWaitTime; private bool isDown = true; private double locationY = -40.0; private bool lanMode; public EventHandler LogoutEvent; public void AddPrimarySwitchable(ISwitchable switchable) { primarySwitches.Add(switchable); btnMainButton.Text = switchable.GetSwitchName() + " (F2)"; } public void RemovePrimarySwitchable(ISwitchable switchable) { primarySwitches.Remove(switchable); btnMainButton.Text = primarySwitches[primarySwitches.Count - 1].GetSwitchName() + " (F2)"; } public void SetSecondarySwitch(ISwitchable switchable) => cncnetLobbySwitch = switchable; public void SetTertiarySwitch(ISwitchable switchable) => privateMessageSwitch = switchable; public void SetOptionsWindow(OptionsWindow optionsWindow) { this.optionsWindow = optionsWindow; optionsWindow.EnabledChanged += OptionsWindow_EnabledChanged; } private void OptionsWindow_EnabledChanged(object sender, EventArgs e) { if (!lanMode) SetSwitchButtonsClickable(!optionsWindow.Enabled); SetOptionsButtonClickable(!optionsWindow.Enabled); if (optionsWindow != null) optionsWindow.ToggleMainMenuOnlyOptions(primarySwitches.Count == 1 && !lanMode); } public void Clean() { if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel(); } public override void Initialize() { Name = "TopBar"; ClientRectangle = new Rectangle(0, -39, WindowManager.RenderResolutionX, 39); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; BackgroundTexture = AssetLoader.CreateTexture(Color.Black, 1, 1); DrawBorders = false; btnMainButton = new XNAClientButton(WindowManager); btnMainButton.Name = nameof(btnMainButton); btnMainButton.ClientRectangle = new Rectangle(12, 9, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT); btnMainButton.Text = "Main Menu (F2)".L10N("Client:Main:MainMenuF2"); btnMainButton.LeftClick += BtnMainButton_LeftClick; btnCnCNetLobby = new XNAClientButton(WindowManager); btnCnCNetLobby.Name = nameof(btnCnCNetLobby); btnCnCNetLobby.ClientRectangle = new Rectangle(184, 9, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT); btnCnCNetLobby.Text = "CnCNet Lobby (F3)".L10N("Client:Main:LobbyF3"); btnCnCNetLobby.LeftClick += BtnCnCNetLobby_LeftClick; btnPrivateMessages = new XNAClientButton(WindowManager); btnPrivateMessages.Name = nameof(btnPrivateMessages); btnPrivateMessages.ClientRectangle = new Rectangle(356, 9, UIDesignConstants.BUTTON_WIDTH_160, UIDesignConstants.BUTTON_HEIGHT); btnPrivateMessages.Text = DEFAULT_PM_BTN_LABEL; btnPrivateMessages.LeftClick += BtnPrivateMessages_LeftClick; lblDate = new XNALabel(WindowManager); lblDate.Name = nameof(lblDate); lblDate.FontIndex = 1; lblDate.Text = Renderer.GetSafeString(DateTime.Now.ToShortDateString(), lblDate.FontIndex); lblDate.ClientRectangle = new Rectangle(Width - (int)Renderer.GetTextDimensions(lblDate.Text, lblDate.FontIndex).X - 12, 18, lblDate.Width, lblDate.Height); lblTime = new XNALabel(WindowManager); lblTime.Name = nameof(lblTime); lblTime.FontIndex = 1; lblTime.Text = Renderer.GetSafeString(new DateTime(1, 1, 1, 23, 59, 59).ToLongTimeString(), lblTime.FontIndex); lblTime.ClientRectangle = new Rectangle(Width - (int)Renderer.GetTextDimensions(lblTime.Text, lblTime.FontIndex).X - 12, 4, lblTime.Width, lblTime.Height); btnLogout = new XNAClientButton(WindowManager); btnLogout.Name = nameof(btnLogout); btnLogout.ClientRectangle = new Rectangle(lblDate.X - 87, 9, 75, 23); btnLogout.FontIndex = 1; btnLogout.Text = "Log Out".L10N("Client:Main:TopBarLogOut"); btnLogout.AllowClick = false; btnLogout.LeftClick += BtnLogout_LeftClick; btnOptions = new XNAClientButton(WindowManager); btnOptions.Name = nameof(btnOptions); btnOptions.ClientRectangle = new Rectangle(btnLogout.X - 122, 9, 110, 23); btnOptions.Text = "Options (F12)".L10N("Client:Main:OptionsF12"); btnOptions.LeftClick += BtnOptions_LeftClick; lblConnectionStatus = new XNALabel(WindowManager); lblConnectionStatus.Name = "lblConnectionStatus"; lblConnectionStatus.FontIndex = 1; lblConnectionStatus.Text = "OFFLINE".L10N("Client:Main:StatusOffline"); AddChild(btnMainButton); AddChild(btnCnCNetLobby); AddChild(btnPrivateMessages); AddChild(btnOptions); AddChild(lblTime); AddChild(lblDate); AddChild(btnLogout); AddChild(lblConnectionStatus); if (ClientConfiguration.Instance.DisplayPlayerCountInTopBar) { lblCnCNetStatus = new XNALabel(WindowManager); lblCnCNetStatus.Name = "lblCnCNetStatus"; lblCnCNetStatus.FontIndex = 1; lblCnCNetStatus.Text = ClientConfiguration.Instance.LocalGame.ToUpper() + " " + "PLAYERS ONLINE:".L10N("Client:Main:OnlinePlayersNumber"); lblCnCNetPlayerCount = new XNALabel(WindowManager); lblCnCNetPlayerCount.Name = "lblCnCNetPlayerCount"; lblCnCNetPlayerCount.FontIndex = 1; lblCnCNetPlayerCount.Text = "-"; lblCnCNetPlayerCount.ClientRectangle = new Rectangle(btnOptions.X - 50, 11, lblCnCNetPlayerCount.Width, lblCnCNetPlayerCount.Height); lblCnCNetStatus.ClientRectangle = new Rectangle(lblCnCNetPlayerCount.X - lblCnCNetStatus.Width - 6, 11, lblCnCNetStatus.Width, lblCnCNetStatus.Height); AddChild(lblCnCNetStatus); AddChild(lblCnCNetPlayerCount); CnCNetPlayerCountTask.CnCNetGameCountUpdated += CnCNetInfoController_CnCNetGameCountUpdated; cncnetPlayerCountCancellationSource = new CancellationTokenSource(); CnCNetPlayerCountTask.InitializeService(cncnetPlayerCountCancellationSource); } lblConnectionStatus.CenterOnParent(); base.Initialize(); Keyboard.OnKeyPressed += Keyboard_OnKeyPressed; connectionManager.Connected += ConnectionManager_Connected; connectionManager.Disconnected += ConnectionManager_Disconnected; connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; connectionManager.WelcomeMessageReceived += ConnectionManager_WelcomeMessageReceived; connectionManager.AttemptedServerChanged += ConnectionManager_AttemptedServerChanged; connectionManager.ConnectAttemptFailed += ConnectionManager_ConnectAttemptFailed; privateMessageHandler.UnreadMessageCountUpdated += PrivateMessageHandler_UnreadMessageCountUpdated; } private void PrivateMessageHandler_UnreadMessageCountUpdated(object sender, UnreadMessageCountEventArgs args) => UpdatePrivateMessagesBtnLabel(args.UnreadMessageCount); private void UpdatePrivateMessagesBtnLabel(int unreadMessageCount) { btnPrivateMessages.Text = DEFAULT_PM_BTN_LABEL; if (unreadMessageCount > 0) { // TODO need to make a wider button to accommodate count // btnPrivateMessages.Text += $" ({unreadMessageCount})"; } } private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCountEventArgs e) { lock (locker) { if (e.PlayerCount == -1) lblCnCNetPlayerCount.Text = "N/A".L10N("Client:Main:N/A"); else lblCnCNetPlayerCount.Text = e.PlayerCount.ToString(); } } private void ConnectionManager_ConnectionLost(object sender, Online.EventArguments.ConnectionLostEventArgs e) { if (!lanMode) ConnectionEvent("OFFLINE".L10N("Client:Main:StatusOffline")); } private void ConnectionManager_ConnectAttemptFailed(object sender, EventArgs e) { if (!lanMode) ConnectionEvent("OFFLINE".L10N("Client:Main:StatusOffline")); } private void ConnectionManager_AttemptedServerChanged(object sender, Online.EventArguments.AttemptedServerEventArgs e) { ConnectionEvent("CONNECTING...".L10N("Client:Main:StatusConnecting")); BringDown(); } private void ConnectionManager_WelcomeMessageReceived(object sender, Online.EventArguments.ServerMessageEventArgs e) => ConnectionEvent("CONNECTED".L10N("Client:Main:StatusConnected")); private void ConnectionManager_Disconnected(object sender, EventArgs e) { btnLogout.AllowClick = false; if (!lanMode) ConnectionEvent("OFFLINE".L10N("Client:Main:StatusOffline")); } private void ConnectionEvent(string text) { lblConnectionStatus.Text = text; lblConnectionStatus.CenterOnParent(); isDown = true; downTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS - EVENT_DOWN_TIME_WAIT_SECONDS); } private void BtnLogout_LeftClick(object sender, EventArgs e) { connectionManager.Disconnect(); LogoutEvent?.Invoke(this, null); SwitchToPrimary(); } private void ConnectionManager_Connected(object sender, EventArgs e) => btnLogout.AllowClick = true; public void SwitchToPrimary() => BtnMainButton_LeftClick(this, EventArgs.Empty); public ISwitchable GetTopMostPrimarySwitchable() => primarySwitches[primarySwitches.Count - 1]; public void SwitchToSecondary() => BtnCnCNetLobby_LeftClick(this, EventArgs.Empty); private void BtnCnCNetLobby_LeftClick(object sender, EventArgs e) { LastSwitchType = SwitchType.SECONDARY; primarySwitches[primarySwitches.Count - 1].SwitchOff(); cncnetLobbySwitch.SwitchOn(); privateMessageSwitch.SwitchOff(); // HACK warning // TODO: add a way for DarkeningPanel to skip transitions ((DarkeningPanel)((XNAControl)cncnetLobbySwitch).Parent).Alpha = 1.0f; } private void BtnMainButton_LeftClick(object sender, EventArgs e) { LastSwitchType = SwitchType.PRIMARY; cncnetLobbySwitch.SwitchOff(); privateMessageSwitch.SwitchOff(); primarySwitches[primarySwitches.Count - 1].SwitchOn(); // HACK warning // TODO: add a way for DarkeningPanel to skip transitions if (((XNAControl)primarySwitches[primarySwitches.Count - 1]).Parent is DarkeningPanel darkeningPanel) darkeningPanel.Alpha = 1.0f; } private void BtnPrivateMessages_LeftClick(object sender, EventArgs e) => privateMessageSwitch.SwitchOn(); private void BtnOptions_LeftClick(object sender, EventArgs e) { privateMessageSwitch.SwitchOff(); optionsWindow.Open(); } private void Keyboard_OnKeyPressed(object sender, KeyPressEventArgs e) { if (!Enabled || !WindowManager.HasFocus || ProgramConstants.IsInGame) return; switch (e.PressedKey) { case Keys.F1: BringDown(); break; case Keys.F2 when btnMainButton.AllowClick: BtnMainButton_LeftClick(this, EventArgs.Empty); break; case Keys.F3 when btnCnCNetLobby.AllowClick: BtnCnCNetLobby_LeftClick(this, EventArgs.Empty); break; case Keys.F4 when btnPrivateMessages.AllowClick: BtnPrivateMessages_LeftClick(this, EventArgs.Empty); break; case Keys.F12 when btnOptions.AllowClick: BtnOptions_LeftClick(this, EventArgs.Empty); break; } } public override void OnMouseOnControl() { if (Cursor.Location.Y > -1 && !ProgramConstants.IsInGame) BringDown(); base.OnMouseOnControl(); } void BringDown() { isDown = true; downTime = TimeSpan.Zero; } public void SetMainButtonText(string text) => btnMainButton.Text = text; public void SetSwitchButtonsClickable(bool allowClick) { if (btnMainButton != null) btnMainButton.AllowClick = allowClick; if (btnCnCNetLobby != null) btnCnCNetLobby.AllowClick = allowClick; if (btnPrivateMessages != null) btnPrivateMessages.AllowClick = allowClick; } public void SetOptionsButtonClickable(bool allowClick) { if (btnOptions != null) btnOptions.AllowClick = allowClick; } public void SetLanMode(bool lanMode) { this.lanMode = lanMode; SetSwitchButtonsClickable(!lanMode); if (lanMode) ConnectionEvent("LAN MODE".L10N("Client:Main:StatusLanMode")); else ConnectionEvent("OFFLINE".L10N("Client:Main:StatusOffline")); } public override void Update(GameTime gameTime) { if (Cursor.Location.Y < APPEAR_CURSOR_THRESHOLD_Y && Cursor.Location.Y > -1 && !ProgramConstants.IsInGame) BringDown(); if (isDown) { if (locationY < 0) { locationY += DOWN_MOVEMENT_RATE * (gameTime.ElapsedGameTime.TotalMilliseconds / 10.0); ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); } downTime += gameTime.ElapsedGameTime; isDown = downTime < downTimeWaitTime; } else { if (locationY > -Height - 1) { locationY -= UP_MOVEMENT_RATE * (gameTime.ElapsedGameTime.TotalMilliseconds / 10.0); ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); } else return; // Don't handle input when the cursor is above our game window } DateTime dtn = DateTime.Now; lblTime.Text = Renderer.GetSafeString(dtn.ToLongTimeString(), lblTime.FontIndex); string dateText = Renderer.GetSafeString(dtn.ToShortDateString(), lblDate.FontIndex); if (lblDate.Text != dateText) lblDate.Text = dateText; base.Update(gameTime); } public override void Draw(GameTime gameTime) { base.Draw(gameTime); Renderer.DrawRectangle(new Rectangle(X, ClientRectangle.Bottom - 2, Width, 1), UISettings.ActiveSettings.PanelBorderColor); } } public enum SwitchType { PRIMARY, SECONDARY } } ================================================ FILE: DXMainClient/DXGUI/Generic/URLHandler.cs ================================================ #nullable enable using System; using System.Linq; using ClientCore; using ClientCore.Extensions; using ClientGUI; using Rampastring.Tools; using Rampastring.XNAUI; namespace DTAClient.DXGUI.Generic { public static class URLHandler { /// /// Checks whether a URL is safe before opening it, prompting a warning as an XNAMessageBox otherwise. /// public static void OpenLink(WindowManager wm, string url) { // Determine if the links is trusted bool isTrusted = false; try { string domain = new Uri(url).Host; var trustedDomains = ClientConfiguration.Instance.TrustedDomains.Concat(ClientConfiguration.Instance.AlwaysTrustedDomains); isTrusted = trustedDomains.Contains(domain, StringComparer.InvariantCultureIgnoreCase) || trustedDomains.Any(trustedDomain => domain.EndsWith("." + trustedDomain, StringComparison.InvariantCultureIgnoreCase)); } catch (Exception ex) { isTrusted = false; Logger.Log($"Error in parsing the URL \"{url}\": {ex.ToString()}"); } if (isTrusted) { ProcessLauncher.StartShellProcess(url); return; } // Show the warning if the links is not trusted var msgBox = new XNAMessageBox(wm, "Open Link Confirmation".L10N("Client:Main:OpenLinkConfirmationTitle"), """ You're about to open a link shared in chat. Please note that this link hasn't been verified, and CnCNet is not responsible for its content. Would you like to open the following link in your browser? """.L10N("Client:Main:OpenLinkConfirmationText") + Environment.NewLine + Environment.NewLine + url, XNAMessageBoxButtons.YesNo); msgBox.YesClickedAction = (msgBox) => ProcessLauncher.StartShellProcess(url); msgBox.Show(); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/UpdateQueryWindow.cs ================================================ using ClientCore; using ClientGUI; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Generic { /// /// A window that asks the user whether they want to update their game. /// public class UpdateQueryWindow : XNAWindow { public delegate void UpdateAcceptedEventHandler(object sender, EventArgs e); public event UpdateAcceptedEventHandler UpdateAccepted; public delegate void UpdateDeclinedEventHandler(object sender, EventArgs e); public event UpdateDeclinedEventHandler UpdateDeclined; public UpdateQueryWindow(WindowManager windowManager) : base(windowManager) { } private XNALabel lblDescription; private XNALabel lblUpdateSize; private string changelogUrl; public override void Initialize() { changelogUrl = ClientConfiguration.Instance.ChangelogURL; Name = "UpdateQueryWindow"; ClientRectangle = new Rectangle(0, 0, 251, 140); BackgroundTexture = AssetLoader.LoadTexture("updatequerybg.png"); lblDescription = new XNALabel(WindowManager); lblDescription.ClientRectangle = new Rectangle(12, 9, 0, 0); lblDescription.Text = String.Empty; lblDescription.Name = nameof(lblDescription); var lblChangelogLink = new XNALinkLabel(WindowManager); lblChangelogLink.ClientRectangle = new Rectangle(12, 50, 0, 0); lblChangelogLink.Text = "View Changelog".L10N("Client:Main:ViewChangeLog"); lblChangelogLink.IdleColor = Color.Goldenrod; lblChangelogLink.Name = nameof(lblChangelogLink); lblChangelogLink.LeftClick += LblChangelogLink_LeftClick; lblUpdateSize = new XNALabel(WindowManager); lblUpdateSize.ClientRectangle = new Rectangle(12, 80, 0, 0); lblUpdateSize.Text = String.Empty; lblUpdateSize.Name = nameof(lblUpdateSize); var btnYes = new XNAClientButton(WindowManager); btnYes.ClientRectangle = new Rectangle(12, 110, 75, 23); btnYes.Text = "Yes".L10N("Client:Main:ButtonYes"); btnYes.LeftClick += BtnYes_LeftClick; btnYes.Name = nameof(btnYes); var btnNo = new XNAClientButton(WindowManager); btnNo.ClientRectangle = new Rectangle(164, 110, 75, 23); btnNo.Text = "No".L10N("Client:Main:ButtonNo"); btnNo.LeftClick += BtnNo_LeftClick; btnNo.Name = nameof(btnNo); AddChild(lblDescription); AddChild(lblChangelogLink); AddChild(lblUpdateSize); AddChild(btnYes); AddChild(btnNo); base.Initialize(); CenterOnParent(); } private void LblChangelogLink_LeftClick(object sender, EventArgs e) { ProcessLauncher.StartShellProcess(changelogUrl); } private void BtnYes_LeftClick(object sender, EventArgs e) { UpdateAccepted?.Invoke(this, e); } private void BtnNo_LeftClick(object sender, EventArgs e) { UpdateDeclined?.Invoke(this, e); } public void SetInfo(string version, int updateSize) { lblDescription.Text = string.Format(("Version {0} is available for download.\nDo you wish to install it?").L10N("Client:Main:VersionAvailable"), version); if (updateSize >= 1000) lblUpdateSize.Text = string.Format("The size of the update is {0} MB.".L10N("Client:Main:UpdateSizeMB"), updateSize / 1000); else lblUpdateSize.Text = string.Format("The size of the update is {0} KB.".L10N("Client:Main:UpdateSizeKB"), updateSize); } } } ================================================ FILE: DXMainClient/DXGUI/Generic/UpdateWindow.cs ================================================ using ClientGUI; using DTAClient.Domain; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; #if WINFORMS using System.Runtime.InteropServices; #endif using ClientUpdater; namespace DTAClient.DXGUI.Generic { /// /// The update window, displaying the update progress to the user. /// public class UpdateWindow : XNAWindow { public delegate void UpdateCancelEventHandler(object sender, EventArgs e); public event UpdateCancelEventHandler UpdateCancelled; public delegate void UpdateCompletedEventHandler(object sender, EventArgs e); public event UpdateCompletedEventHandler UpdateCompleted; public delegate void UpdateFailureEventHandler(object sender, UpdateFailureEventArgs e); public event UpdateFailureEventHandler UpdateFailed; delegate void UpdateProgressChangedDelegate(string fileName, int filePercentage, int totalPercentage); delegate void FileDownloadCompletedDelegate(string archiveName); private const double DOT_TIME = 0.66; private const int MAX_DOTS = 5; public UpdateWindow(WindowManager windowManager) : base(windowManager) { } private XNALabel lblDescription; private XNALabel lblCurrentFileProgressPercentageValue; private XNALabel lblTotalProgressPercentageValue; private XNALabel lblCurrentFile; private XNALabel lblUpdaterStatus; private XNAProgressBar prgCurrentFile; private XNAProgressBar prgTotal; #if WINFORMS private TaskbarProgress tbp; #endif private bool isStartingForceUpdate; bool infoUpdated = false; string currFileName = string.Empty; int currFilePercentage = 0; int totalPercentage = 0; int dotCount = 0; double currentDotTime = 0.0; private static readonly object locker = new object(); public override void Initialize() { Name = "UpdateWindow"; ClientRectangle = new Rectangle(0, 0, 446, 270); BackgroundTexture = AssetLoader.LoadTexture("updaterbg.png"); lblDescription = new XNALabel(WindowManager); lblDescription.Text = string.Empty; lblDescription.ClientRectangle = new Rectangle(12, 9, 0, 0); lblDescription.Name = nameof(lblDescription); var lblCurrentFileProgressPercentage = new XNALabel(WindowManager); lblCurrentFileProgressPercentage.Text = "Progress percentage of current file:".L10N("Client:Main:CurrentFileProgressPercentage"); lblCurrentFileProgressPercentage.ClientRectangle = new Rectangle(12, 90, 0, 0); lblCurrentFileProgressPercentage.Name = nameof(lblCurrentFileProgressPercentage); lblCurrentFileProgressPercentageValue = new XNALabel(WindowManager); lblCurrentFileProgressPercentageValue.Text = "0%"; lblCurrentFileProgressPercentageValue.ClientRectangle = new Rectangle(409, lblCurrentFileProgressPercentage.Y, 0, 0); lblCurrentFileProgressPercentageValue.Name = nameof(lblCurrentFileProgressPercentageValue); prgCurrentFile = new XNAProgressBar(WindowManager); prgCurrentFile.Name = nameof(prgCurrentFile); prgCurrentFile.Maximum = 100; prgCurrentFile.ClientRectangle = new Rectangle(12, 110, 422, 30); //prgCurrentFile.BorderColor = UISettings.WindowBorderColor; prgCurrentFile.SmoothForwardTransition = true; prgCurrentFile.SmoothTransitionRate = 10; lblCurrentFile = new XNALabel(WindowManager); lblCurrentFile.Name = nameof(lblCurrentFile); lblCurrentFile.ClientRectangle = new Rectangle(12, 142, 0, 0); var lblTotalProgressPercentage = new XNALabel(WindowManager); lblTotalProgressPercentage.Text = "Total progress percentage:".L10N("Client:Main:TotalProgressPercentage"); lblTotalProgressPercentage.ClientRectangle = new Rectangle(12, 170, 0, 0); lblTotalProgressPercentage.Name = nameof(lblTotalProgressPercentage); lblTotalProgressPercentageValue = new XNALabel(WindowManager); lblTotalProgressPercentageValue.Text = "0%"; lblTotalProgressPercentageValue.ClientRectangle = new Rectangle(409, lblTotalProgressPercentage.Y, 0, 0); lblTotalProgressPercentageValue.Name = nameof(lblTotalProgressPercentageValue); prgTotal = new XNAProgressBar(WindowManager); prgTotal.Name = nameof(prgTotal); prgTotal.Maximum = 100; prgTotal.ClientRectangle = new Rectangle(12, 190, prgCurrentFile.Width, prgCurrentFile.Height); //prgTotal.BorderColor = UISettings.WindowBorderColor; lblUpdaterStatus = new XNALabel(WindowManager); lblUpdaterStatus.Name = nameof(lblUpdaterStatus); lblUpdaterStatus.Text = "Preparing".L10N("Client:Main:StatusPreparing"); lblUpdaterStatus.ClientRectangle = new Rectangle(12, 240, 0, 0); var btnCancel = new XNAClientButton(WindowManager); btnCancel.ClientRectangle = new Rectangle(301, 240, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(lblDescription); AddChild(lblCurrentFileProgressPercentage); AddChild(lblCurrentFileProgressPercentageValue); AddChild(prgCurrentFile); AddChild(lblCurrentFile); AddChild(lblTotalProgressPercentage); AddChild(lblTotalProgressPercentageValue); AddChild(prgTotal); AddChild(lblUpdaterStatus); AddChild(btnCancel); base.Initialize(); // Read theme settings from INI CenterOnParent(); Updater.FileIdentifiersUpdated += Updater_FileIdentifiersUpdated; Updater.OnUpdateCompleted += Updater_OnUpdateCompleted; Updater.OnUpdateFailed += Updater_OnUpdateFailed; Updater.UpdateProgressChanged += Updater_UpdateProgressChanged; Updater.LocalFileCheckProgressChanged += Updater_LocalFileCheckProgressChanged; Updater.OnFileDownloadCompleted += Updater_OnFileDownloadCompleted; #if WINFORMS tbp = new TaskbarProgress(); #endif } private void Updater_FileIdentifiersUpdated() { if (!isStartingForceUpdate) return; if (Updater.VersionState == VersionState.UNKNOWN) { XNAMessageBox.Show(WindowManager, "Force Update Failure".L10N("Client:Main:ForceUpdateFailureTitle"), "Checking for updates failed.".L10N("Client:Main:ForceUpdateFailureText")); AddCallback(new Action(CloseWindow), null); return; } else if (Updater.VersionState == VersionState.OUTDATED && Updater.ManualUpdateRequired) { UpdateCancelled?.Invoke(this, EventArgs.Empty); AddCallback(new Action(CloseWindow), null); return; } SetData(Updater.ServerGameVersion); Updater.StartUpdate(); isStartingForceUpdate = false; } private void Updater_LocalFileCheckProgressChanged(int checkedFileCount, int totalFileCount) { AddCallback(new Action(UpdateFileProgress), (checkedFileCount * 100 / totalFileCount)); } private void UpdateFileProgress(int value) { prgCurrentFile.Value = value; lblCurrentFileProgressPercentageValue.Text = value + "%"; } private void Updater_UpdateProgressChanged(string currFileName, int currFilePercentage, int totalPercentage) { lock (locker) { infoUpdated = true; this.currFileName = currFileName; this.currFilePercentage = currFilePercentage; this.totalPercentage = totalPercentage; } } private void HandleUpdateProgressChange() { if (!infoUpdated) return; infoUpdated = false; if (currFilePercentage < 0 || currFilePercentage > prgCurrentFile.Maximum) prgCurrentFile.Value = 0; else prgCurrentFile.Value = currFilePercentage; if (totalPercentage < 0 || totalPercentage > prgTotal.Maximum) prgTotal.Value = 0; else prgTotal.Value = totalPercentage; lblCurrentFileProgressPercentageValue.Text = prgCurrentFile.Value.ToString() + "%"; lblTotalProgressPercentageValue.Text = prgTotal.Value.ToString() + "%"; lblCurrentFile.Text = "Current file:".L10N("Client:Main:CurrentFile") + " " + currFileName; lblUpdaterStatus.Text = "Downloading files".L10N("Client:Main:DownloadingFiles"); #if WINFORMS /*/ TODO Improve the updater * When the updater thread in DTAUpdater.dll has completed the update, it will * restart the client right away without giving the UI thread a chance to * finish its tasks and free resources in a proper way. * Because of that, this function is sometimes executed when * the game window has already been hidden / removed, and the code below * will then crash the client, causing the user to see a KABOOM message * along with the successful update, which is likely quite confusing for the user. * The try-catch is a dirty temporary workaround. * /*/ try { tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.Normal); tbp.SetValue(WindowManager.GetWindowHandle(), prgTotal.Value, prgTotal.Maximum); } catch { } #endif } private void Updater_OnFileDownloadCompleted(string archiveName) { AddCallback(new FileDownloadCompletedDelegate(HandleFileDownloadCompleted), archiveName); } private void HandleFileDownloadCompleted(string archiveName) { lblUpdaterStatus.Text = "Unpacking archive".L10N("Client:Main:UnpackingArchive"); } private void Updater_OnUpdateCompleted() { AddCallback(new Action(HandleUpdateCompleted), null); } private void HandleUpdateCompleted() { #if WINFORMS tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.NoProgress); #endif UpdateCompleted?.Invoke(this, EventArgs.Empty); } private void Updater_OnUpdateFailed(Exception ex) { AddCallback(new Action(HandleUpdateFailed), ex.Message); } private void HandleUpdateFailed(string updateFailureErrorMessage) { #if WINFORMS tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.NoProgress); #endif UpdateFailed?.Invoke(this, new UpdateFailureEventArgs(updateFailureErrorMessage)); } private void BtnCancel_LeftClick(object sender, EventArgs e) { if (!isStartingForceUpdate) Updater.StopUpdate(); CloseWindow(); } private void CloseWindow() { isStartingForceUpdate = false; #if WINFORMS tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.NoProgress); #endif UpdateCancelled?.Invoke(this, EventArgs.Empty); } public void SetData(string newGameVersion) { lblDescription.Text = string.Format(("Please wait while {0} is updated to version {1}.\nThis window will automatically close once the update is complete.\n\nThe client may also restart after the update has been downloaded.").L10N("Client:Main:UpdateVersionPleaseWait"), MainClientConstants.GAME_NAME_SHORT, newGameVersion); lblUpdaterStatus.Text = "Preparing".L10N("Client:Main:StatusPreparing"); } public void ForceUpdate() { isStartingForceUpdate = true; lblDescription.Text = string.Format("Force updating {0} to latest version...".L10N("Client:Main:ForceUpdateToLatest"), MainClientConstants.GAME_NAME_SHORT); lblUpdaterStatus.Text = "Connecting".L10N("Client:Main:UpdateStatusConnecting"); Updater.CheckForUpdates(); } public override void Update(GameTime gameTime) { base.Update(gameTime); lock (locker) { HandleUpdateProgressChange(); } currentDotTime += gameTime.ElapsedGameTime.TotalSeconds; if (currentDotTime > DOT_TIME) { currentDotTime = 0.0; dotCount++; if (dotCount > MAX_DOTS) dotCount = 0; } } public override void Draw(GameTime gameTime) { base.Draw(gameTime); float xOffset = 3.0f; for (int i = 0; i < dotCount; i++) { var wrect = lblUpdaterStatus.RenderRectangle(); Renderer.DrawStringWithShadow(".", lblUpdaterStatus.FontIndex, new Vector2(wrect.Right + xOffset, wrect.Bottom - 15.0f), lblUpdaterStatus.TextColor); xOffset += 3.0f; } } } public class UpdateFailureEventArgs : EventArgs { public UpdateFailureEventArgs(string reason) { this.reason = reason; } string reason = String.Empty; /// /// The returned error message from the update failure. /// public string Reason { get { return reason; } } } #if WINFORMS /// /// For utilizing the taskbar progress bar introduced in Windows 7: /// http://stackoverflow.com/questions/1295890/windows-7-progress-bar-in-taskbar-in-c /// public class TaskbarProgress { public enum TaskbarStates { NoProgress = 0, Indeterminate = 0x1, Normal = 0x2, Error = 0x4, Paused = 0x8 } [ComImportAttribute()] [GuidAttribute("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")] [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] private interface ITaskbarList3 { // ITaskbarList [PreserveSig] void HrInit(); [PreserveSig] void AddTab(IntPtr hwnd); [PreserveSig] void DeleteTab(IntPtr hwnd); [PreserveSig] void ActivateTab(IntPtr hwnd); [PreserveSig] void SetActiveAlt(IntPtr hwnd); // ITaskbarList2 [PreserveSig] void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen); // ITaskbarList3 [PreserveSig] void SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal); [PreserveSig] void SetProgressState(IntPtr hwnd, TaskbarStates state); } [GuidAttribute("56FDF344-FD6D-11d0-958A-006097C9A090")] [ClassInterfaceAttribute(ClassInterfaceType.None)] [ComImportAttribute()] private class TaskbarInstance { } private ITaskbarList3 taskbarInstance = (ITaskbarList3)new TaskbarInstance(); public void SetState(IntPtr windowHandle, TaskbarStates taskbarState) { taskbarInstance.SetProgressState(windowHandle, taskbarState); } public void SetValue(IntPtr windowHandle, double progressValue, double progressMax) { taskbarInstance.SetProgressValue(windowHandle, (ulong)progressValue, (ulong)progressMax); } } #endif } ================================================ FILE: DXMainClient/DXGUI/IGameSessionSetting.cs ================================================ using DTAClient.Domain.Multiplayer; using Rampastring.Tools; namespace DTAClient.DXGUI; // TODO split the logic between campaign/mp and clean up public interface IGameSessionSetting { /// Gets the name of this setting. string Name { get; } /// Indicates whether this setting can affect spawn.ini. bool AffectsSpawnIni { get; } /// Indicates whether this setting can affect map code. bool AffectsMapCode { get; } /// Indicates whether this setting in its current state allows the game to be scored. bool AllowScoring { get; } /// Indicates whether this setting should be broadcast to the lobby. bool BroadcastToLobby { get; } /// /// Gets or sets the value of this setting. /// For checkboxes: 0 = unchecked/off, 1 = checked/on. /// For dropdowns: the selected index. /// int Value { get; set; } /// Applies the associated code to the spawn.ini file. /// The spawn.ini file. void ApplySpawnIniCode(IniFile spawnIni); /// Applies the associated code to the map INI file. /// The map INI file. /// Currently selected gamemode, if applicable. void ApplyMapCode(IniFile mapIni, GameMode gameMode); } ================================================ FILE: DXMainClient/DXGUI/IMessageView.cs ================================================ using DTAClient.Online; namespace DTAClient.DXGUI { public interface IMessageView { void AddMessage(ChatMessage message); } } ================================================ FILE: DXMainClient/DXGUI/ISwitchable.cs ================================================ namespace DTAClient.DXGUI { /// /// An interface for all switchable windows. /// public interface ISwitchable { void SwitchOn(); void SwitchOff(); string GetSwitchName(); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/ChatListBox.cs ================================================ using System; using ClientCore; using ClientCore.Extensions; using DTAClient.DXGUI.Generic; using DTAClient.Online; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { /// /// A list box for CnCNet chat. Supports opening links with a double-click, /// and easy adding of IRC messages to the list box. /// public class ChatListBox : XNAListBox, IMessageView { public ChatListBox(WindowManager windowManager) : base(windowManager) { DoubleLeftClick += ChatListBox_DoubleLeftClick; } private void ChatListBox_DoubleLeftClick(object sender, EventArgs e) { if (SelectedIndex < 0 || SelectedIndex >= Items.Count) return; // Get the clicked links string[] links = Items[SelectedIndex].Text?.GetLinks(); if (links == null) return; if (links.Length == 0 || links.Length > 1) return; string link = links[0]; URLHandler.OpenLink(WindowManager, link); } public void AddMessage(string message) { AddMessage(new ChatMessage(message)); } public void AddMessage(string sender, string message, Color color) { AddMessage(new ChatMessage(sender, color, DateTime.Now, message)); } public void AddMessage(ChatMessage message) { var listBoxItem = new XNAListBoxItem { TextColor = message.Color, Selectable = true, Tag = message }; if (message.SenderName == null) { listBoxItem.Text = Renderer.GetSafeString(string.Format("[{0}] {1}", message.DateTime.ToShortTimeString(), message.Message), FontIndex); } else { listBoxItem.Text = Renderer.GetSafeString(string.Format("[{0}] {1}: {2}", message.DateTime.ToShortTimeString(), message.SenderName, message.Message), FontIndex); } AddItem(listBoxItem); if (LastIndex >= Items.Count - 2) { ScrollToBottom(); } } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/ChoiceNotificationBox.cs ================================================ using ClientGUI; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.IO; using System.Reflection; using DTAClient.Domain.Multiplayer.CnCNet; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A box that allows users to make a choice, /// top-left of the game window. /// public class ChoiceNotificationBox : XNAPanel { private const double DOWN_TIME_WAIT_SECONDS = 4.0; private const double DOWN_MOVEMENT_RATE = 2.0; private const double UP_MOVEMENT_RATE = 2.0; public ChoiceNotificationBox(WindowManager windowManager) : base(windowManager) { downTimeWaitTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS); } public Action AffirmativeClickedAction { get; set; } public Action NegativeClickedAction { get; set; } private XNALabel lblHeader; private XNAPanel gameIconPanel; private XNALabel lblSender; private XNALabel lblChoiceText; private XNAClientButton affirmativeButton; private XNAClientButton negativeButton; private TimeSpan downTime = TimeSpan.Zero; private TimeSpan downTimeWaitTime; private bool isDown = false; private const int boxHeight = 101; private double locationY = -boxHeight; public override void Initialize() { Name = nameof(ChoiceNotificationBox); BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 196), 1, 1); ClientRectangle = new Rectangle(0, -boxHeight, 300, boxHeight); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lblHeader = new XNALabel(WindowManager); lblHeader.Name = nameof(lblHeader); lblHeader.FontIndex = 1; lblHeader.AnchorPoint = new Vector2(ClientRectangle.Width / 2, 12); lblHeader.TextAnchor = LabelTextAnchorInfo.CENTER; lblHeader.Text = "MAKE A CHOICE".L10N("Client:Main:MakeAChoice"); AddChild(lblHeader); using Stream dtaIconStream = Assembly.GetAssembly(typeof(GameCollection)).GetManifestResourceStream("DTAClient.Icons.dtaicon.png"); using var dtaIcon = Image.Load(dtaIconStream); gameIconPanel = new XNAPanel(WindowManager); gameIconPanel.Name = nameof(gameIconPanel); gameIconPanel.ClientRectangle = new Rectangle(12, lblHeader.Bottom + 6, 16, 16); gameIconPanel.DrawBorders = false; gameIconPanel.BackgroundTexture = AssetLoader.TextureFromImage(dtaIcon); AddChild(gameIconPanel); lblSender = new XNALabel(WindowManager); lblSender.Name = nameof(lblSender); lblSender.FontIndex = 1; lblSender.ClientRectangle = new Rectangle(gameIconPanel.Right + 3, lblHeader.Bottom + 6, 0, 0); lblSender.Text = "fonger"; AddChild(lblSender); lblChoiceText = new XNALabel(WindowManager); lblChoiceText.Name = nameof(lblChoiceText); lblChoiceText.FontIndex = 1; lblChoiceText.ClientRectangle = new Rectangle(12, lblSender.Bottom + 6, 0, 0); lblChoiceText.Text = "What do you want to do?".L10N("Client:Main:ChoiceWhatDoYouWant"); AddChild(lblChoiceText); affirmativeButton = new XNAClientButton(WindowManager); affirmativeButton.ClientRectangle = new Rectangle(ClientRectangle.Left + 8, lblChoiceText.Bottom + 6, 75, 23); affirmativeButton.Name = nameof(affirmativeButton); affirmativeButton.Text = "Yes".L10N("Client:Main:ButtonYes"); affirmativeButton.LeftClick += AffirmativeButton_LeftClick; AddChild(affirmativeButton); negativeButton = new XNAClientButton(WindowManager); negativeButton.ClientRectangle = new Rectangle(ClientRectangle.Width - (75 + 8), lblChoiceText.Bottom + 6, 75, 23); negativeButton.Name = nameof(negativeButton); negativeButton.Text = "No".L10N("Client:Main:ButtonNo"); negativeButton.LeftClick += NegativeButton_LeftClick; AddChild(negativeButton); base.Initialize(); } // a timeout of zero means the notification will never be automatically dismissed public void Show( string headerText, Texture2D gameIcon, string sender, string choiceText, string affirmativeText, string negativeText, int timeout = 0) { Enable(); lblHeader.Text = headerText; gameIconPanel.BackgroundTexture = gameIcon; lblSender.Text = sender; lblChoiceText.Text = choiceText; affirmativeButton.Text = affirmativeText; negativeButton.Text = negativeText; // use the same clipping logic as the PM notification if (lblChoiceText.Width > Width) { while (lblChoiceText.Width > Width) { lblChoiceText.Text = lblChoiceText.Text.Remove(lblChoiceText.Text.Length - 1); } } downTime = TimeSpan.Zero; isDown = true; downTimeWaitTime = TimeSpan.FromSeconds(timeout); } public void Hide() { isDown = false; locationY = -Height; ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); Disable(); } public override void Update(GameTime gameTime) { if (isDown) { if (locationY < 0) { locationY += DOWN_MOVEMENT_RATE; ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); } if (WindowManager.HasFocus) { downTime += gameTime.ElapsedGameTime; // only change our "down" state if we have a valid timeout if (downTimeWaitTime != TimeSpan.Zero) { isDown = downTime < downTimeWaitTime; } } } else { if (locationY > -Height) { locationY -= UP_MOVEMENT_RATE; ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); } else { // effectively delete ourselves when we've timed out WindowManager.RemoveControl(this); } } base.Update(gameTime); } private void AffirmativeButton_LeftClick(object sender, EventArgs e) { AffirmativeClickedAction?.Invoke(this); WindowManager.RemoveControl(this); } private void NegativeButton_LeftClick(object sender, EventArgs e) { NegativeClickedAction?.Invoke(this); WindowManager.RemoveControl(this); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs ================================================ using ClientCore; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Generic; using DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers; using DTAClient.Online; using DTAClient.Online.EventArguments; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.Text; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A game lobby for loading saved CnCNet games. /// public class CnCNetGameLoadingLobby : GameLoadingLobbyBase { private const double GAME_BROADCAST_INTERVAL = 20.0; private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; private const string NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND = "NPRSNT"; private const string GET_READY_CTCP_COMMAND = "GTRDY"; private const string FILE_HASH_CTCP_COMMAND = "FHSH"; private const string INVALID_FILE_HASH_CTCP_COMMAND = "IHSH"; private const string TUNNEL_PING_CTCP_COMMAND = "TNLPNG"; private const string OPTIONS_CTCP_COMMAND = "OP"; private const string INVALID_SAVED_GAME_INDEX_CTCP_COMMAND = "ISGI"; private const string START_GAME_CTCP_COMMAND = "START"; private const string PLAYER_READY_CTCP_COMMAND = "READY"; private const string CHANGE_TUNNEL_SERVER_MESSAGE = "CHTNL"; public CnCNetGameLoadingLobby( WindowManager windowManager, TopBar topBar, CnCNetManager connectionManager, TunnelHandler tunnelHandler, MapLoader mapLoader, GameCollection gameCollection, DiscordHandler discordHandler, CnCNetUserData cncnetUserData ) : base(windowManager, discordHandler) { this.connectionManager = connectionManager; this.tunnelHandler = tunnelHandler; this.topBar = topBar; this.gameCollection = gameCollection; this.mapLoader = mapLoader; this.cncnetUserData = cncnetUserData; ctcpCommandHandlers = new CommandHandlerBase[] { new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, HandleNotAllPresentNotification), new NoParamCommandHandler(GET_READY_CTCP_COMMAND, HandleGetReadyNotification), new StringCommandHandler(FILE_HASH_CTCP_COMMAND, HandleFileHashCommand), new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, HandleCheaterNotification), new IntCommandHandler(TUNNEL_PING_CTCP_COMMAND, HandleTunnelPing), new StringCommandHandler(OPTIONS_CTCP_COMMAND, HandleOptionsMessage), new NoParamCommandHandler(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, HandleInvalidSaveIndexCommand), new StringCommandHandler(START_GAME_CTCP_COMMAND, HandleStartGameCommand), new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, HandlePlayerReadyRequest), new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage) }; } private CommandHandlerBase[] ctcpCommandHandlers; private CnCNetManager connectionManager; private CnCNetUserData cncnetUserData; private List gameModes; private TunnelHandler tunnelHandler; private readonly MapLoader mapLoader; private TunnelSelectionWindow tunnelSelectionWindow; private XNAClientButton btnChangeTunnel; private Channel channel; private GameCollection gameCollection; private IRCColor chatColor; private string hostName; private string localGame; private string gameFilesHash; private XNATimerControl gameBroadcastTimer; private bool started; private DarkeningPanel dp; private TopBar topBar; public override void Initialize() { dp = new DarkeningPanel(WindowManager); //WindowManager.AddAndInitializeControl(dp); //dp.AddChildWithoutInitialize(this); //dp.Alpha = 0.0f; //dp.Hide(); localGame = ClientConfiguration.Instance.LocalGame; base.Initialize(); connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; connectionManager.Disconnected += ConnectionManager_Disconnected; tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); tunnelSelectionWindow.Initialize(); tunnelSelectionWindow.DrawOrder = 1; tunnelSelectionWindow.UpdateOrder = 1; DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); tunnelSelectionWindow.CenterOnParent(); tunnelSelectionWindow.Disable(); tunnelSelectionWindow.TunnelSelected += TunnelSelectionWindow_TunnelSelected; btnChangeTunnel = new XNAClientButton(WindowManager); btnChangeTunnel.Name = nameof(btnChangeTunnel); btnChangeTunnel.ClientRectangle = new Rectangle(btnLeaveGame.Right - btnLeaveGame.Width - 145, btnLeaveGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnChangeTunnel.Text = "Change Tunnel".L10N("Client:Main:ChangeTunnel"); btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; AddChild(btnChangeTunnel); gameBroadcastTimer = new XNATimerControl(WindowManager); gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = false; gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed; WindowManager.AddAndInitializeControl(gameBroadcastTimer); } public override void Refresh(bool isHost) { base.Refresh(isHost); btnChangeTunnel.Visible = isHost; gameBroadcastTimer.Enabled = isHost; } private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame(); private void ConnectionManager_Disconnected(object sender, EventArgs e) => Clear(); private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => Clear(); /// /// Sets up events and information before joining the channel. /// public void SetUp(bool isHost, CnCNetTunnel tunnel, Channel channel, string hostName) { this.channel = channel; this.hostName = hostName; channel.MessageAdded += Channel_MessageAdded; channel.UserAdded += Channel_UserAdded; channel.UserLeft += Channel_UserLeft; channel.UserQuitIRC += Channel_UserQuitIRC; channel.CTCPReceived += Channel_CTCPReceived; tunnelHandler.CurrentTunnel = tunnel; tunnelHandler.CurrentTunnelPinged += TunnelHandler_CurrentTunnelPinged; started = false; Refresh(isHost); } private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) { // TODO Rampastring pls, review and merge that XNAIndicator PR already } /// /// Clears event subscriptions and leaves the channel. /// public void Clear() { gameBroadcastTimer.Enabled = false; if (channel != null) { // TODO leave channel only if we've joined the channel channel.Leave(); channel.MessageAdded -= Channel_MessageAdded; channel.UserAdded -= Channel_UserAdded; channel.UserLeft -= Channel_UserLeft; channel.UserQuitIRC -= Channel_UserQuitIRC; channel.CTCPReceived -= Channel_CTCPReceived; connectionManager.RemoveChannel(channel); } if (Enabled) { Enabled = false; Visible = false; base.LeaveGame(); } tunnelHandler.CurrentTunnel = null; tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; topBar.RemovePrimarySwitchable(this); } private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) { foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers) { if (cmdHandler.Handle(e.UserName, e.Message)) return; } Logger.Log("Unhandled CTCP command: " + e.Message + " from " + e.UserName); } /// /// Called when the local user has joined the game channel. /// public void OnJoined() { FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(); if (IsHost) { connectionManager.SendCustomMessage(new QueuedMessage( string.Format("MODE {0} +klnNs {1} {2}", channel.ChannelName, channel.Password, SGPlayers.Count), QueuedMessageType.SYSTEM_MESSAGE, 50)); connectionManager.SendCustomMessage(new QueuedMessage( string.Format("TOPIC {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), QueuedMessageType.SYSTEM_MESSAGE, 50)); gameFilesHash = fhc.GetCompleteHash(); gameBroadcastTimer.Enabled = true; gameBroadcastTimer.Start(); gameBroadcastTimer.SetTime(TimeSpan.FromSeconds(INITIAL_GAME_BROADCAST_DELAY)); } else { channel.SendCTCPMessage(FILE_HASH_CTCP_COMMAND + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10); channel.SendCTCPMessage(TUNNEL_PING_CTCP_COMMAND + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); if (tunnelHandler.CurrentTunnel.PingInMs < 0) AddNotice(string.Format("{0} - unknown ping to tunnel server.".L10N("Client:Main:PlayerUnknownPing"), ProgramConstants.PLAYERNAME)); else AddNotice(string.Format("{0} - ping to tunnel server: {1} ms".L10N("Client:Main:PlayerPing"), ProgramConstants.PLAYERNAME, tunnelHandler.CurrentTunnel.PingInMs)); } topBar.AddPrimarySwitchable(this); topBar.SwitchToPrimary(); WindowManager.SelectedControl = tbChatInput; UpdateDiscordPresence(true); } private void Channel_UserAdded(object sender, ChannelUserEventArgs e) { PlayerInfo pInfo = new PlayerInfo(); pInfo.Name = e.User.IRCUser.Name; Players.Add(pInfo); sndJoinSound.Play(); BroadcastOptions(); CopyPlayerDataToUI(); UpdateDiscordPresence(); } private void Channel_UserLeft(object sender, UserNameEventArgs e) { RemovePlayer(e.UserName); UpdateDiscordPresence(); } private void Channel_UserQuitIRC(object sender, UserNameEventArgs e) { RemovePlayer(e.UserName); UpdateDiscordPresence(); } private void RemovePlayer(string playerName) { int index = Players.FindIndex(p => p.Name == playerName); if (index == -1) return; sndLeaveSound.Play(); Players.RemoveAt(index); CopyPlayerDataToUI(); if (!IsHost && playerName == hostName && !ProgramConstants.IsInGame) { connectionManager.MainChannel.AddMessage(new ChatMessage( Color.Yellow, "The game host left the game!".L10N("Client:Main:HostLeft"))); Clear(); } } private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) { if (!string.IsNullOrEmpty(e.Message.SenderIdent) && cncnetUserData.IsIgnored(e.Message.SenderIdent) && !e.Message.SenderIsAdmin) { lbChatMessages.AddMessage(new ChatMessage(Color.Silver, string.Format("Message blocked from - {0}".L10N("Client:Main:PMBlockedFrom"), e.Message.SenderName))); } else { lbChatMessages.AddMessage(e.Message); sndMessageSound.Play(); } } protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message)); protected override void BroadcastOptions() { if (!IsHost) return; //if (Players.Count > 0) Players[0].Ready = true; StringBuilder message = new StringBuilder(OPTIONS_CTCP_COMMAND + " "); message.Append(ddSavedGame.SelectedIndex); message.Append(";"); foreach (PlayerInfo pInfo in Players) { message.Append(pInfo.Name); message.Append(":"); message.Append(Convert.ToInt32(pInfo.Ready)); message.Append(";"); } message.Remove(message.Length - 1, 1); channel.SendCTCPMessage(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10); } protected override void SendChatMessage(string message) { sndMessageSound.Play(); channel.SendChatMessage(message, chatColor); } protected override void RequestReadyStatus() => channel.SendCTCPMessage(PLAYER_READY_CTCP_COMMAND + " 1", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10); protected override void GetReadyNotification() { base.GetReadyNotification(); topBar.SwitchToPrimary(); if (IsHost) channel.SendCTCPMessage(GET_READY_CTCP_COMMAND, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } protected override void NotAllPresentNotification() { base.NotAllPresentNotification(); if (IsHost) { channel.SendCTCPMessage(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } } private void ShowTunnelSelectionWindow(string description) { tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel?.Address); } private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e) { channel.SendCTCPMessage($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", QueuedMessageType.SYSTEM_MESSAGE, 10); HandleTunnelServerChange(e.Tunnel); } #region CTCP Handlers private void HandleGetReadyNotification(string sender) { if (sender != hostName) return; GetReadyNotification(); } private void HandleNotAllPresentNotification(string sender) { if (sender != hostName) return; NotAllPresentNotification(); } private void HandleFileHashCommand(string sender, string fileHash) { if (!IsHost) return; PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; pInfo.HashReceived = true; if (fileHash != gameFilesHash) HandleCheaterNotification(hostName, sender); // This is kinda hacky } private void HandleCheaterNotification(string sender, string cheaterName) { if (sender != hostName) return; AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); if (IsHost) channel.SendCTCPMessage(INVALID_FILE_HASH_CTCP_COMMAND + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); } private void HandleTunnelPing(string sender, int pingInMs) { if (pingInMs < 0) AddNotice(string.Format("{0} - unknown ping to tunnel server.".L10N("Client:Main:PlayerUnknownPing"), sender)); else AddNotice(string.Format("{0} - ping to tunnel server: {1} ms".L10N("Client:Main:PlayerPing"), sender, pingInMs)); } /// /// Handles an options broadcast sent by the game host. /// private void HandleOptionsMessage(string sender, string data) { if (sender != hostName) return; string[] parts = data.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 1) return; int sgIndex = Conversions.IntFromString(parts[0], -1); if (sgIndex < 0) return; if (sgIndex >= ddSavedGame.Items.Count) { AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); channel.SendCTCPMessage(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10); return; } ddSavedGame.SelectedIndex = sgIndex; Players.Clear(); for (int i = 1; i < parts.Length; i++) { string[] playerAndReadyStatus = parts[i].Split(':'); if (playerAndReadyStatus.Length < 2) return; string playerName = playerAndReadyStatus[0]; int readyStatus = Conversions.IntFromString(playerAndReadyStatus[1], -1); if (string.IsNullOrEmpty(playerName) || readyStatus == -1) return; PlayerInfo pInfo = new PlayerInfo(); pInfo.Name = playerName; pInfo.Ready = Convert.ToBoolean(readyStatus); Players.Add(pInfo); } CopyPlayerDataToUI(); } private void HandleInvalidSaveIndexCommand(string sender) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; pInfo.Ready = false; AddNotice(string.Format("{0} does not have the selected saved game on their system! Try selecting an earlier saved game.".L10N("Client:Main:PlayerDontHaveSavedGame"), pInfo.Name)); CopyPlayerDataToUI(); } private void HandleStartGameCommand(string sender, string data) { if (sender != hostName) return; string[] parts = data.Split(';'); int playerCount = parts.Length / 2; for (int i = 0; i < playerCount; i++) { if (parts.Length < i * 2 + 1) return; string pName = parts[i * 2]; string ipAndPort = parts[i * 2 + 1]; string[] ipAndPortSplit = ipAndPort.Split(':'); if (ipAndPortSplit.Length < 2) return; int port = 0; bool success = int.TryParse(ipAndPortSplit[1], out port); if (!success) return; PlayerInfo pInfo = Players.Find(p => p.Name == pName); if (pInfo == null) continue; pInfo.Port = port; } LoadGame(); } private void HandlePlayerReadyRequest(string sender, int readyStatus) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; pInfo.Ready = Convert.ToBoolean(readyStatus); CopyPlayerDataToUI(); if (IsHost) BroadcastOptions(); } private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort) { if (sender != hostName) return; string[] split = tunnelAddressAndPort.Split(':'); string tunnelAddress = split[0]; int tunnelPort = int.Parse(split[1]); CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); if (tunnel == null) { AddNotice(("The game host has selected an invalid tunnel server! " + "The game host needs to change the server or you will be unable " + "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), Color.Yellow); btnLoadGame.AllowClick = false; return; } HandleTunnelServerChange(tunnel); btnLoadGame.AllowClick = true; } /// /// Changes the tunnel server used for the game. /// /// The new tunnel server to use. private void HandleTunnelServerChange(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); //UpdatePing(); } #endregion protected override void HostStartGame() { AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(SGPlayers.Count); if (playerPorts.Count < Players.Count) { ShowTunnelSelectionWindow(("An error occured while contacting the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); AddNotice(("An error occured while contacting the specified CnCNet " + "tunnel server. Please try using a different tunnel server").L10N("Client:Main:ConnectTunnelError2") + " ", Color.Yellow); return; } StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + " "); for (int pId = 0; pId < Players.Count; pId++) { Players[pId].Port = playerPorts[pId]; sb.Append(Players[pId].Name); sb.Append(";"); sb.Append("0.0.0.0:"); sb.Append(playerPorts[pId]); sb.Append(";"); } sb.Remove(sb.Length - 1, 1); channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9); AddNotice("Starting game...".L10N("Client:Main:StartingGame")); started = true; LoadGame(); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) { spawnIni.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); spawnIni.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); base.WriteSpawnIniAdditions(spawnIni); } protected override void HandleGameProcessExited() { base.HandleGameProcessExited(); Clear(); } protected override void LeaveGame() => Clear(); public void ChangeChatColor(IRCColor chatColor) { this.chatColor = chatColor; tbChatInput.TextColor = chatColor.XnaColor; } private void BroadcastGame() { Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); if (broadcastChannel == null) return; StringBuilder sb = new StringBuilder("GAME "); sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); sb.Append(";"); sb.Append(ProgramConstants.GAME_VERSION); sb.Append(";"); sb.Append(SGPlayers.Count); sb.Append(";"); sb.Append(channel.ChannelName); sb.Append(";"); sb.Append(channel.UIName); sb.Append(";"); if (started || Players.Count == SGPlayers.Count) sb.Append("1"); else sb.Append("0"); sb.Append("0"); // IsCustomPassword sb.Append("0"); // Closed sb.Append("1"); // IsLoadedGame sb.Append("0"); // IsLadder sb.Append(";"); foreach (SavedGamePlayer sgPlayer in SGPlayers) { sb.Append(sgPlayer.Name); sb.Append(","); } sb.Remove(sb.Length - 1, 1); sb.Append(";"); sb.Append((string)lblMapNameValue.Tag); sb.Append(";"); sb.Append((string)lblGameModeValue.Tag); sb.Append(";"); sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); sb.Append(";"); sb.Append(0); // LoadedGameId sb.Append(";"); sb.Append(ClientConfiguration.Instance.DefaultSkillLevelIndex); // we don't know the original skill level sb.Append(";"); // Map SHA1 sb.Append(";"); // Game option values broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } public override string GetSwitchName() => "Load Game".L10N("Client:Main:LoadGame"); protected override void UpdateDiscordPresence(bool resetTimer = false) { if (discordHandler == null) return; PlayerInfo player = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (player == null) return; string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings discordHandler.UpdatePresence( (string)lblMapNameValue.Tag, (string)lblGameModeValue.Tag, "Multiplayer", currentState, Players.Count, SGPlayers.Count, channel.UIName, IsHost, resetTimer); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs ================================================ using ClientCore; using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Generic; using DTAClient.DXGUI.Multiplayer.GameLobby; using DTAClient.Online; using DTAClient.Online.EventArguments; using DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using ClientCore.Enums; using ClientCore.Extensions; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer.CnCNet { using UserChannelPair = Tuple; using InvitationIndex = Dictionary, WeakReference>; internal class CnCNetLobby : XNAWindow, ISwitchable { public event EventHandler UpdateCheck; public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, CnCNetGameLobby gameLobby, CnCNetGameLoadingLobby gameLoadingLobby, TopBar topBar, PrivateMessagingWindow pmWindow, TunnelHandler tunnelHandler, GameCollection gameCollection, CnCNetUserData cncnetUserData, OptionsWindow optionsWindow, MapLoader mapLoader, Random random) : base(windowManager) { this.connectionManager = connectionManager; this.gameLobby = gameLobby; this.gameLoadingLobby = gameLoadingLobby; this.tunnelHandler = tunnelHandler; this.topBar = topBar; this.pmWindow = pmWindow; this.gameCollection = gameCollection; this.cncnetUserData = cncnetUserData; this.optionsWindow = optionsWindow; this.mapLoader = mapLoader; this.random = random; ctcpCommandHandlers = new CommandHandlerBase[] { new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, HandleGameInviteCommand), new NoParamCommandHandler(ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND, HandleGameInvitationFailedNotification) }; topBar.LogoutEvent += LogoutEvent; } private MapLoader mapLoader; private CnCNetManager connectionManager; private CnCNetUserData cncnetUserData; private readonly OptionsWindow optionsWindow; private PlayerListBox lbPlayerList; private ChatListBox lbChatMessages; private GameListBox lbGameList; private GlobalContextMenu globalContextMenu; private XNAClientButton btnLogout; private XNAClientButton btnNewGame; private XNAClientButton btnJoinGame; private XNAChatTextBox tbChatInput; private XNALabel lblColor; private XNALabel lblCurrentChannel; private XNALabel lblOnline; private XNALabel lblOnlineCount; private XNAClientDropDown ddColor; private XNAClientDropDown ddCurrentChannel; private XNASuggestionTextBox tbGameSearch; private XNAClientStateButton btnGameSortAlpha; private XNAClientToggleButton btnGameFilterOptions; private DarkeningPanel gameCreationPanel; private Channel currentChatChannel; private GameCollection gameCollection; private Color cAdminNameColor; private Texture2D unknownGameIcon; private Texture2D adminGameIcon; private EnhancedSoundEffect sndGameCreated; private EnhancedSoundEffect sndGameInviteReceived; private IRCColor[] chatColors; private CnCNetGameLobby gameLobby; private CnCNetGameLoadingLobby gameLoadingLobby; private TunnelHandler tunnelHandler; private CnCNetLoginWindow loginWindow; private TopBar topBar; private PrivateMessagingWindow pmWindow; private PasswordRequestWindow passwordRequestWindow; private bool isInGameRoom = false; private bool updateDenied = false; private string localGameID; private CnCNetGame localGame; private List followedGames = new List(); private bool isJoiningGame = false; private HostedCnCNetGame gameOfLastJoinAttempt; private CancellationTokenSource gameCheckCancellation; private CommandHandlerBase[] ctcpCommandHandlers; private InvitationIndex invitationIndex; private GameFiltersPanel panelGameFilters; private Random random; private bool ctcpInvalidGameMessageShown = false; private bool ctcpNoTunnelMessageShown = false; private bool ctcpNoTunnelForGamesMessageShown = false; private void GameList_ClientRectangleUpdated(object sender, EventArgs e) { panelGameFilters.ClientRectangle = lbGameList.ClientRectangle; } private void LogoutEvent(object sender, EventArgs e) { isJoiningGame = false; } public override void Initialize() { invitationIndex = new InvitationIndex(); ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 64, WindowManager.RenderResolutionY - 64); Name = nameof(CnCNetLobby); BackgroundTexture = AssetLoader.LoadTexture("cncnetlobbybg.png"); localGameID = ClientConfiguration.Instance.LocalGame; localGame = gameCollection.GameList.Find(g => g.InternalName.ToUpper() == localGameID.ToUpper()); btnNewGame = new XNAClientButton(WindowManager); btnNewGame.Name = nameof(btnNewGame); btnNewGame.ClientRectangle = new Rectangle(12, Height - 29, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnNewGame.Text = "Create Game".L10N("Client:Main:CreateGame"); btnNewGame.AllowClick = false; btnNewGame.LeftClick += BtnNewGame_LeftClick; btnJoinGame = new XNAClientButton(WindowManager); btnJoinGame.Name = nameof(btnJoinGame); btnJoinGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnJoinGame.Text = "Join Game".L10N("Client:Main:JoinGame"); btnJoinGame.AllowClick = false; btnJoinGame.LeftClick += BtnJoinGame_LeftClick; btnLogout = new XNAClientButton(WindowManager); btnLogout.Name = nameof(btnLogout); btnLogout.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLogout.Text = "Log Out".L10N("Client:Main:LogOut"); btnLogout.LeftClick += BtnLogout_LeftClick; var gameListRectangle = new Rectangle( btnNewGame.X, 41, btnJoinGame.Right - btnNewGame.X, btnNewGame.Y - 47 ); panelGameFilters = new GameFiltersPanel(WindowManager, gameLobby); panelGameFilters.Name = nameof(panelGameFilters); panelGameFilters.ClientRectangle = gameListRectangle; panelGameFilters.Disable(); lbGameList = new GameListBox(WindowManager, mapLoader, localGameID, gameLobby, HostedGameMatches); lbGameList.Name = nameof(lbGameList); lbGameList.ClientRectangle = gameListRectangle; lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbGameList.DoubleLeftClick += LbGameList_DoubleLeftClick; lbGameList.RightClick += LbGameList_RightClick; lbGameList.AllowMultiLineItems = false; lbGameList.ClientRectangleUpdated += GameList_ClientRectangleUpdated; lbPlayerList = new PlayerListBox(WindowManager, gameCollection); lbPlayerList.Name = nameof(lbPlayerList); lbPlayerList.ClientRectangle = new Rectangle(Width - 202, 20, 190, btnLogout.Y - 26); lbPlayerList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbPlayerList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbPlayerList.LineHeight = 16; lbPlayerList.DoubleLeftClick += LbPlayerList_DoubleLeftClick; lbPlayerList.RightClick += LbPlayerList_RightClick; globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow); globalContextMenu.JoinEvent += (sender, args) => JoinUser(args.IrcUser, connectionManager.MainChannel); lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = nameof(lbChatMessages); lbChatMessages.ClientRectangle = new Rectangle(lbGameList.Right + 12, lbGameList.Y, lbPlayerList.X - lbGameList.Right - 24, lbPlayerList.Height); lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbChatMessages.LineHeight = 16; lbChatMessages.LeftClick += (sender, args) => lbGameList.SelectedIndex = -1; lbChatMessages.RightClick += LbChatMessages_RightClick; tbChatInput = new XNAChatTextBox(WindowManager); tbChatInput.Name = nameof(tbChatInput); tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X, btnNewGame.Y, lbChatMessages.Width, btnNewGame.Height); tbChatInput.Suggestion = "Type here to chat...".L10N("Client:Main:ChatHere"); tbChatInput.Enabled = false; tbChatInput.MaximumTextLength = 200; tbChatInput.EnterPressed += TbChatInput_EnterPressed; lblColor = new XNALabel(WindowManager); lblColor.Name = nameof(lblColor); lblColor.ClientRectangle = new Rectangle(lbChatMessages.X, 14, 0, 0); lblColor.FontIndex = 1; lblColor.Text = "YOUR COLOR:".L10N("Client:Main:YourColor"); ddColor = new XNAClientDropDown(WindowManager); ddColor.Name = nameof(ddColor); ddColor.ClientRectangle = new Rectangle(lblColor.X + 95, 12, 150, 21); chatColors = connectionManager.GetIRCColors(); foreach (IRCColor color in connectionManager.GetIRCColors()) { if (!color.Selectable) continue; XNADropDownItem ddItem = new XNADropDownItem(); ddItem.Text = color.Name; ddItem.TextColor = color.XnaColor; ddItem.Tag = color; ddColor.AddItem(ddItem); } int selectedColor = UserINISettings.Instance.ChatColor; ddColor.SelectedIndex = selectedColor >= ddColor.Items.Count || selectedColor < 0 ? ClientConfiguration.Instance.DefaultPersonalChatColorIndex : selectedColor; SetChatColor(); ddColor.SelectedIndexChanged += DdColor_SelectedIndexChanged; ddCurrentChannel = new XNAClientDropDown(WindowManager); ddCurrentChannel.Name = nameof(ddCurrentChannel); ddCurrentChannel.ClientRectangle = new Rectangle( lbChatMessages.Right - 200, ddColor.Y, 200, 21); ddCurrentChannel.SelectedIndexChanged += DdCurrentChannel_SelectedIndexChanged; ddCurrentChannel.AllowDropDown = false; lblCurrentChannel = new XNALabel(WindowManager); lblCurrentChannel.Name = nameof(lblCurrentChannel); lblCurrentChannel.ClientRectangle = new Rectangle( ddCurrentChannel.X - 150, ddCurrentChannel.Y + 2, 0, 0); lblCurrentChannel.FontIndex = 1; lblCurrentChannel.Text = "CURRENT CHANNEL:".L10N("Client:Main:CurrentChannel"); lblOnline = new XNALabel(WindowManager); lblOnline.Name = nameof(lblOnline); lblOnline.ClientRectangle = new Rectangle(310, 14, 0, 0); lblOnline.Text = "Online:".L10N("Client:Main:OnlineLabel"); lblOnline.FontIndex = 1; lblOnline.Disable(); lblOnlineCount = new XNALabel(WindowManager); lblOnlineCount.Name = nameof(lblOnlineCount); lblOnlineCount.ClientRectangle = new Rectangle(lblOnline.X + 50, 14, 0, 0); lblOnlineCount.FontIndex = 1; lblOnlineCount.Disable(); tbGameSearch = new XNASuggestionTextBox(WindowManager); tbGameSearch.Name = nameof(tbGameSearch); tbGameSearch.ClientRectangle = new Rectangle(lbGameList.X, 12, lbGameList.Width - 62, 21); tbGameSearch.Suggestion = "Filter by name, map, game mode, player...".L10N("Client:Main:FilterByBlahBlah"); tbGameSearch.MaximumTextLength = 64; tbGameSearch.InputReceived += TbGameSearch_InputReceived; tbGameSearch.Disable(); btnGameSortAlpha = new XNAClientStateButton(WindowManager, new Dictionary() { { SortDirection.None, AssetLoader.LoadTexture("sortAlphaNone.png") }, { SortDirection.Asc, AssetLoader.LoadTexture("sortAlphaAsc.png") }, { SortDirection.Desc, AssetLoader.LoadTexture("sortAlphaDesc.png") }, }); btnGameSortAlpha.Name = nameof(btnGameSortAlpha); btnGameSortAlpha.ClientRectangle = new Rectangle( tbGameSearch.X + tbGameSearch.Width + 10, tbGameSearch.Y, 21, 21); btnGameSortAlpha.LeftClick += BtnGameSortAlpha_LeftClick; btnGameSortAlpha.SetToolTipText("Sort Games Alphabetically".L10N("Client:Main:SortAlphabet")); RefreshGameSortAlphaBtn(); btnGameFilterOptions = new XNAClientToggleButton(WindowManager); btnGameFilterOptions.Name = nameof(btnGameFilterOptions); btnGameFilterOptions.ClientRectangle = new Rectangle( btnGameSortAlpha.X + btnGameSortAlpha.Width + 10, tbGameSearch.Y, 21, 21); btnGameFilterOptions.CheckedTexture = AssetLoader.LoadTexture("filterActive.png"); btnGameFilterOptions.UncheckedTexture = AssetLoader.LoadTexture("filterInactive.png"); btnGameFilterOptions.LeftClick += BtnGameFilterOptions_LeftClick; btnGameFilterOptions.SetToolTipText("Game Filters".L10N("Client:Main:GameFilters")); RefreshGameFiltersBtn(); InitializeGameList(); AddChild(btnNewGame); AddChild(btnJoinGame); AddChild(btnLogout); AddChild(lbPlayerList); AddChild(lbChatMessages); AddChild(lbGameList); AddChild(panelGameFilters); AddChild(tbChatInput); AddChild(lblColor); AddChild(ddColor); AddChild(lblCurrentChannel); AddChild(ddCurrentChannel); AddChild(globalContextMenu); AddChild(lblOnline); AddChild(lblOnlineCount); AddChild(tbGameSearch); AddChild(btnGameSortAlpha); AddChild(btnGameFilterOptions); panelGameFilters.VisibleChanged += GameFiltersPanel_VisibleChanged; CnCNetPlayerCountTask.CnCNetGameCountUpdated += OnCnCNetGameCountUpdated; UpdateOnlineCount(CnCNetPlayerCountTask.PlayerCount); pmWindow.SetJoinUserAction(JoinUser); base.Initialize(); WindowManager.CenterControlOnScreen(this); PostUIInit(); } private void BtnGameSortAlpha_LeftClick(object sender, EventArgs e) { UserINISettings.Instance.SortState.Value = (int)btnGameSortAlpha.GetState(); RefreshGameSortAlphaBtn(); SortAndRefreshHostedGames(); UserINISettings.Instance.SaveSettings(); } private void SortAndRefreshHostedGames() { lbGameList.SortAndRefreshHostedGames(); } private void BtnGameFilterOptions_LeftClick(object sender, EventArgs e) { if (panelGameFilters.Visible) panelGameFilters.Cancel(); else panelGameFilters.Show(); } private void RefreshGameSortAlphaBtn() { if (Enum.IsDefined(typeof(SortDirection), UserINISettings.Instance.SortState.Value)) btnGameSortAlpha.SetState((SortDirection)UserINISettings.Instance.SortState.Value); } private void RefreshGameFiltersBtn() { btnGameFilterOptions.Checked = UserINISettings.Instance.IsGameFiltersApplied(); } private void GameFiltersPanel_VisibleChanged(object sender, EventArgs e) { if (panelGameFilters.Visible) return; RefreshGameFiltersBtn(); SortAndRefreshHostedGames(); } private void TbGameSearch_InputReceived(object sender, EventArgs e) { SortAndRefreshHostedGames(); lbGameList.ViewTop = 0; } /// /// Checks if a hosted game matches the current filter criteria. /// /// The hosted game to check. /// True if the game matches the filter criteria, false otherwise. private bool HostedGameMatches(GenericHostedGame hg) { // friends list takes priority over other filters below if (UserINISettings.Instance.ShowFriendGamesOnly) return hg.Players.Any(cncnetUserData.IsFriend); if (UserINISettings.Instance.HideLockedGames.Value && hg.Locked) return false; if (UserINISettings.Instance.HideIncompatibleGames.Value && hg.Incompatible) return false; if (UserINISettings.Instance.HidePasswordedGames.Value && hg.Passworded) return false; if (hg.MaxPlayers > UserINISettings.Instance.MaxPlayerCount.Value) return false; if (hg is HostedCnCNetGame cncnetGame && !GameOptionsMatch(cncnetGame)) return false; string textUpper = tbGameSearch?.Text?.ToUpperInvariant(); string translatedGameMode = string.IsNullOrEmpty(hg.GameMode) ? "Unknown".L10N("Client:Main:Unknown") : hg.GameMode.L10N($"INI:GameModes:{hg.GameMode}:UIName", notify: false); string translatedMapName = string.IsNullOrEmpty(hg.Map) ? "Unknown".L10N("Client:Main:Unknown") : mapLoader.TranslatedMapNames.ContainsKey(hg.Map) ? mapLoader.TranslatedMapNames[hg.Map] : null; return string.IsNullOrWhiteSpace(tbGameSearch?.Text) || tbGameSearch.Text == tbGameSearch.Suggestion || hg.RoomName.ToUpperInvariant().Contains(textUpper) || hg.GameMode.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal) || translatedGameMode.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal) || hg.Map.ToUpperInvariant().Contains(textUpper) || (translatedMapName is not null && translatedMapName.ToUpperInvariant().Contains(textUpper)) || hg.Players.Any(pl => pl.ToUpperInvariant().Equals(textUpper, StringComparison.Ordinal)); } /// /// Checks if a game's broadcast options match the current filter criteria. /// /// The hosted game to check. /// True if the game matches the filter criteria, false otherwise. private bool GameOptionsMatch(HostedCnCNetGame game) { if (game.BroadcastedGameOptionValues == null) return true; var broadcastableSettings = gameLobby.GetBroadcastableSettings(); for (int i = 0; i < broadcastableSettings.Count; i++) { if (i >= game.BroadcastedGameOptionValues.Length) break; int? filterValue = UserINISettings.Instance.GetGameOptionFilterValue(broadcastableSettings[i].Name); if (filterValue == null) continue; if (game.BroadcastedGameOptionValues[i] != filterValue.Value) return false; } return true; } private void OnCnCNetGameCountUpdated(object sender, PlayerCountEventArgs e) => UpdateOnlineCount(e.PlayerCount); private void UpdateOnlineCount(int playerCount) => lblOnlineCount.Text = playerCount.ToString(); private void InitializeGameList() { int i = 0; foreach (var game in gameCollection.GameList) { if (!game.Supported || string.IsNullOrEmpty(game.ChatChannel)) { continue; } var item = new XNADropDownItem(); item.Text = game.UIName; item.Texture = game.Texture; ddCurrentChannel.AddItem(item); var chatChannel = connectionManager.FindChannel(game.ChatChannel); if (chatChannel == null) { chatChannel = connectionManager.CreateChannel(game.UIName, game.ChatChannel, true, true, "ra1-derp"); connectionManager.AddChannel(chatChannel); } item.Tag = chatChannel; if (!string.IsNullOrEmpty(game.GameBroadcastChannel)) { var gameBroadcastChannel = connectionManager.FindChannel(game.GameBroadcastChannel); if (gameBroadcastChannel == null) { gameBroadcastChannel = connectionManager.CreateChannel( string.Format("{0} Broadcast Channel".L10N("Client:Main:BroadcastChannel"), game.UIName), game.GameBroadcastChannel, true, false, null); connectionManager.AddChannel(gameBroadcastChannel); } gameBroadcastChannel.CTCPReceived += GameBroadcastChannel_CTCPReceived; gameBroadcastChannel.UserLeft += GameBroadcastChannel_UserLeftOrQuit; gameBroadcastChannel.UserQuitIRC += GameBroadcastChannel_UserLeftOrQuit; gameBroadcastChannel.UserKicked += GameBroadcastChannel_UserLeftOrQuit; } if (game.InternalName.ToUpper() == localGameID.ToUpper()) { ddCurrentChannel.SelectedIndex = i; } i++; } if (connectionManager.MainChannel == null) { // Set CnCNet channel as main channel if no channel found ddCurrentChannel.SelectedIndex = ddCurrentChannel.Items.Count - 1; } } private void PostUIInit() { sndGameCreated = new EnhancedSoundEffect("gamecreated.wav"); sndGameInviteReceived = new EnhancedSoundEffect("pm.wav"); cAdminNameColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AdminNameColor); var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream unknownIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.unknownicon.png"); using Stream cncnetIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.cncneticon.png"); unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream)); connectionManager.WelcomeMessageReceived += ConnectionManager_WelcomeMessageReceived; connectionManager.Disconnected += ConnectionManager_Disconnected; connectionManager.PrivateCTCPReceived += ConnectionManager_PrivateCTCPReceived; cncnetUserData.UserFriendToggled += RefreshPlayerList; cncnetUserData.UserIgnoreToggled += RefreshPlayerList; gameCreationPanel = new DarkeningPanel(WindowManager); AddChild(gameCreationPanel); GameCreationWindow gcw = new GameCreationWindow(WindowManager, tunnelHandler); gameCreationPanel.AddChild(gcw); gameCreationPanel.Tag = gcw; gcw.Cancelled += Gcw_Cancelled; gcw.GameCreated += Gcw_GameCreated; gcw.LoadedGameCreated += Gcw_LoadedGameCreated; gameCreationPanel.Hide(); string clientVersion = GitVersionInformation.AssemblySemVer; #if DEVELOPMENT_BUILD clientVersion = $"{GitVersionInformation.CommitDate} {GitVersionInformation.BranchName}@{GitVersionInformation.ShortSha}"; #endif connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, Renderer.GetSafeString( string.Format("*** CnCNet Client version {0} ***".L10N("Client:Main:CnCNetClientVersionMessageV2"), clientVersion), lbChatMessages.FontIndex))); { string developBuildWarningMessage = "This is a development build of the client. Stability and reliability may not be fully guaranteed.".L10N("Client:Main:DevelopmentBuildWarning"); #if DEVELOPMENT_BUILD if (ClientConfiguration.Instance.ShowDevelopmentBuildWarnings) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.Red, Renderer.GetSafeString( developBuildWarningMessage, lbChatMessages.FontIndex))); } #endif } connectionManager.BannedFromChannel += ConnectionManager_BannedFromChannel; loginWindow = new CnCNetLoginWindow(WindowManager); loginWindow.Connect += LoginWindow_Connect; loginWindow.Cancelled += LoginWindow_Cancelled; var loginWindowPanel = new DarkeningPanel(WindowManager); loginWindowPanel.Alpha = 0.0f; AddChild(loginWindowPanel); loginWindowPanel.AddChild(loginWindow); loginWindow.Disable(); passwordRequestWindow = new PasswordRequestWindow(WindowManager, pmWindow); passwordRequestWindow.PasswordEntered += PasswordRequestWindow_PasswordEntered; var passwordRequestWindowPanel = new DarkeningPanel(WindowManager); passwordRequestWindowPanel.Alpha = 0.0f; AddChild(passwordRequestWindowPanel); passwordRequestWindowPanel.AddChild(passwordRequestWindow); passwordRequestWindow.Disable(); gameLobby.GameLeft += GameLobby_GameLeft; gameLoadingLobby.GameLeft += GameLoadingLobby_GameLeft; UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved; GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted; GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; } /// /// Displays a message when the IRC server has informed that the local user /// has been banned from a channel that they're attempting to join. /// private void ConnectionManager_BannedFromChannel(object sender, ChannelEventArgs e) { var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName); if (game == null) { var chatChannel = connectionManager.FindChannel(e.ChannelName); chatChannel?.AddMessage(new ChatMessage(Color.White, string.Format( "Cannot join chat channel {0}, you're banned!".L10N("Client:Main:PlayerBannedByChannel"), chatChannel.UIName))); return; } connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format( "Cannot join game {0}, you've been banned by the game host!".L10N("Client:Main:PlayerBannedByHost"), game.RoomName))); isJoiningGame = false; if (gameOfLastJoinAttempt != null) { if (gameOfLastJoinAttempt.IsLoadedGame) gameLoadingLobby.Clear(); else gameLobby.Clear(); } } private void SharedUILogic_GameProcessStarted() { connectionManager.SendCustomMessage(new QueuedMessage("AWAY " + (char)58 + "In-game", QueuedMessageType.SYSTEM_MESSAGE, 0)); } private void SharedUILogic_GameProcessExited() { connectionManager.SendCustomMessage(new QueuedMessage("AWAY", QueuedMessageType.SYSTEM_MESSAGE, 0)); } private void Instance_SettingsSaved(object sender, EventArgs e) { if (!connectionManager.IsConnected) return; foreach (CnCNetGame game in gameCollection.GameList) { if (!game.Supported) continue; if (game.InternalName.ToUpper() == localGameID) continue; if (followedGames.Contains(game.InternalName) && !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { connectionManager.FindChannel(game.GameBroadcastChannel).Leave(); followedGames.Remove(game.InternalName); } else if (!followedGames.Contains(game.InternalName) && UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { connectionManager.FindChannel(game.GameBroadcastChannel).Join(); followedGames.Add(game.InternalName); } } } private void LbPlayerList_RightClick(object sender, EventArgs e) { lbPlayerList.SelectedIndex = lbPlayerList.HoveredIndex; if (lbPlayerList.SelectedIndex < 0 || lbPlayerList.SelectedIndex >= lbPlayerList.Items.Count) { return; } var user = (ChannelUser)lbPlayerList.SelectedItem.Tag; globalContextMenu.Show(user, GetCursorPoint()); } private void LbChatMessages_RightClick(object sender, EventArgs e) { var item = lbChatMessages.HoveredItem; var chatMessage = item?.Tag as ChatMessage; ShowPlayerMessageContextMenu(chatMessage); } private void ShowPlayerMessageContextMenu(ChatMessage chatMessage) { lbChatMessages.SelectedIndex = lbChatMessages.HoveredIndex; globalContextMenu.Show(chatMessage, GetCursorPoint()); } /// /// Enables private messaging by PM'ing a user in the player list. /// private void LbPlayerList_DoubleLeftClick(object sender, EventArgs e) { if (lbPlayerList.SelectedItem == null) return; var channelUser = (ChannelUser)lbPlayerList.SelectedItem.Tag; pmWindow.InitPM(channelUser.IRCUser.Name); } /// /// Hides the login dialog once the user has hit Connect on that dialog. /// private void LoginWindow_Connect(object sender, EventArgs e) { connectionManager.Connect(); loginWindow.Disable(); SetLogOutButtonText(); } /// /// Hides the login window and the CnCNet lobby if the user /// cancels connecting to CnCNet in the login dialog. /// private void LoginWindow_Cancelled(object sender, EventArgs e) { topBar.SwitchToPrimary(); loginWindow.Disable(); } private void GameLoadingLobby_GameLeft(object sender, EventArgs e) { topBar.SwitchToSecondary(); isInGameRoom = false; SetLogOutButtonText(); // keep the friends window up to date so it can disable the Invite option pmWindow.ClearInviteChannelInfo(); } private void GameLobby_GameLeft(object sender, EventArgs e) { topBar.SwitchToSecondary(); isInGameRoom = false; SetLogOutButtonText(); // keep the friends window up to date so it can disable the Invite option pmWindow.ClearInviteChannelInfo(); } private void SetLogOutButtonText() { if (isInGameRoom) { btnLogout.Text = "Game Lobby".L10N("Client:Main:GameLobby"); return; } if (UserINISettings.Instance.PersistentMode) { btnLogout.Text = "Main Menu".L10N("Client:Main:MainMenu"); return; } btnLogout.Text = "Log Out".L10N("Client:Main:LogOut"); } private void BtnJoinGame_LeftClick(object sender, EventArgs e) => JoinSelectedGame(); private void LbGameList_DoubleLeftClick(object sender, EventArgs e) => JoinSelectedGame(); private void LbGameList_RightClick(object sender, EventArgs e) { lbGameList.SelectedIndex = lbGameList.HoveredIndex; var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; if (listedGame == null) return; globalContextMenu.Show(listedGame.HostName, GetCursorPoint()); } private void PasswordRequestWindow_PasswordEntered(object sender, PasswordEventArgs e) => _JoinGame(e.HostedGame, e.Password); private string GetJoinGameErrorBase() { if (isJoiningGame) return "Cannot join game - joining game in progress. If you believe this is an error, please log out and back in.".L10N("Client:Main:JoinGameErrorInProgress"); if (ProgramConstants.IsInGame) return "Cannot join game while the main game executable is running.".L10N("Client:Main:JoinGameErrorGameRunning"); return null; } /// /// Checks if the user can join a game. /// Returns null if the user can, otherwise returns an error message /// that tells the reason why the user cannot join the game. /// /// The index of the game in the game list box. private string GetJoinGameErrorByIndex(int gameIndex) { if (gameIndex < 0 || gameIndex >= lbGameList.HostedGames.Count) return "Invalid game index".L10N("Client:Main:InvalidGameIndex"); return GetJoinGameErrorBase(); } /// /// Returns an error message if game is not join-able, otherwise null. /// /// /// private string GetJoinGameError(HostedCnCNetGame hg) { if (hg.Game.InternalName.ToUpper() != localGameID.ToUpper()) return string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName)); if (hg.Incompatible && ClientConfiguration.Instance.DisallowJoiningIncompatibleGames) return "Cannot join game. The host is on a different game version than you.".L10N("Client:Main:DisallowJoiningIncompatibleGames"); if (hg.Locked) return string.Format("The game {0} is locked!".L10N("Client:Main:GameLockedWithName"), hg.RoomName); if (hg.IsLoadedGame && !hg.Players.Contains(ProgramConstants.PLAYERNAME)) return "You do not exist in the saved game!".L10N("Client:Main:NotInSavedGame"); return GetJoinGameErrorBase(); } private void JoinSelectedGame() { var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; if (listedGame == null) return; var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame); JoinGameByIndex(hostedGameIndex, string.Empty); } private bool JoinGameByIndex(int gameIndex, string password) { string error = GetJoinGameErrorByIndex(gameIndex); if (!string.IsNullOrEmpty(error)) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, error)); return false; } return JoinGame((HostedCnCNetGame)lbGameList.HostedGames[gameIndex], password, connectionManager.MainChannel); } /// /// Attempt to join a game. /// /// The game to join. /// The password to join with. /// The message view/list to write error messages to. /// private bool JoinGame(HostedCnCNetGame hg, string password, IMessageView messageView) { string error = GetJoinGameError(hg); if (!string.IsNullOrEmpty(error)) { messageView.AddMessage(new ChatMessage(Color.White, error)); return false; } if (isInGameRoom) { topBar.SwitchToPrimary(); return false; } if (hg.GameVersion != ProgramConstants.GAME_VERSION) messageView.AddMessage(new ChatMessage(Color.Yellow, "The game host is on a different game version than you. Version incompatibilities may cause issues.".L10N("Client:Main:JoinGameVersionMismatch"))); if (hg.Passworded) { // only display password dialog if we've not been supplied with a password (invite) if (string.IsNullOrEmpty(password)) { passwordRequestWindow.SetHostedGame(hg); passwordRequestWindow.Enable(); return true; } } else { if (!hg.IsLoadedGame) { password = Utilities.CalculateSHA1ForString (hg.ChannelName).Substring(0, 10); } else { IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini")); password = Utilities.CalculateSHA1ForString( spawnSGIni.GetStringValue("Settings", "GameID", string.Empty)).Substring(0, 10); } } _JoinGame(hg, password); return true; } private void _JoinGame(HostedCnCNetGame hg, string password) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName))); isJoiningGame = true; gameOfLastJoinAttempt = hg; Channel gameChannel = connectionManager.CreateChannel(hg.RoomName, hg.ChannelName, false, true, password); connectionManager.AddChannel(gameChannel); if (hg.IsLoadedGame) { gameLoadingLobby.SetUp(false, hg.TunnelServer, gameChannel, hg.HostName); gameChannel.UserAdded += GameLoadingChannel_UserAdded; //gameChannel.MessageAdded += GameLoadingChannel_MessageAdded; gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_LoadedGame; isJoiningGame = false; } else { gameLobby.SetUp(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, hg.SkillLevel); gameChannel.UserAdded += GameChannel_UserAdded; gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_NewGame; gameChannel.InviteOnlyErrorOnJoin += GameChannel_InviteOnlyErrorOnJoin; gameChannel.ChannelFull += GameChannel_ChannelFull; gameChannel.TargetChangeTooFast += GameChannel_TargetChangeTooFast; } connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + hg.ChannelName + " " + password, QueuedMessageType.INSTANT_MESSAGE, 0)); } private void GameChannel_TargetChangeTooFast(object sender, MessageEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); ClearGameJoinAttempt((Channel)sender); } private void GameChannel_ChannelFull(object sender, EventArgs e) => // We'd do the exact same things here, so we can just call the method below GameChannel_InviteOnlyErrorOnJoin(sender, e); private void GameChannel_InviteOnlyErrorOnJoin(object sender, EventArgs e) { var channel = (Channel)sender; var game = FindGameByChannelName(channel.ChannelName); if (game != null) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("The game {0} is locked!".L10N("Client:Main:GameLockedWithName"), game.RoomName))); game.Locked = true; SortAndRefreshHostedGames(); } else { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("Client:Main:GameLocked"))); } ClearGameJoinAttempt((Channel)sender); } private HostedCnCNetGame FindGameByChannelName(string channelName) { var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == channelName); if (game == null) return null; return (HostedCnCNetGame)game; } private void GameChannel_InvalidPasswordEntered_NewGame(object sender, EventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); ClearGameJoinAttempt((Channel)sender); } private void GameChannel_UserAdded(object sender, Online.ChannelUserEventArgs e) { Channel gameChannel = (Channel)sender; if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { ClearGameChannelEvents(gameChannel); gameLobby.OnJoined(); isInGameRoom = true; SetLogOutButtonText(); } } private void ClearGameJoinAttempt(Channel channel) { ClearGameChannelEvents(channel); gameLobby.Clear(); } private void ClearGameChannelEvents(Channel channel) { channel.UserAdded -= GameChannel_UserAdded; channel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_NewGame; channel.InviteOnlyErrorOnJoin -= GameChannel_InviteOnlyErrorOnJoin; channel.ChannelFull -= GameChannel_ChannelFull; channel.TargetChangeTooFast -= GameChannel_TargetChangeTooFast; isJoiningGame = false; } private void BtnNewGame_LeftClick(object sender, EventArgs e) { if (isInGameRoom) { topBar.SwitchToPrimary(); return; } gameCreationPanel.Show(); var gcw = (GameCreationWindow)gameCreationPanel.Tag; gcw.Refresh(); } private void Gcw_GameCreated(object sender, GameCreationEventArgs e) { if (gameLobby.Enabled || gameLoadingLobby.Enabled) return; string channelName = RandomizeChannelName(); string password = e.Password; bool isCustomPassword = true; if (string.IsNullOrEmpty(password)) { password = Utilities.CalculateSHA1ForString( channelName).Substring(0, 10); isCustomPassword = false; } Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); connectionManager.AddChannel(gameChannel); gameLobby.SetUp(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, e.SkillLevel); gameChannel.UserAdded += GameChannel_UserAdded; //gameChannel.MessageAdded += GameChannel_MessageAdded; connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + channelName + " " + password, QueuedMessageType.INSTANT_MESSAGE, 0)); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); gameCreationPanel.Hide(); // update the friends window so it can enable the Invite option pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); } private void Gcw_LoadedGameCreated(object sender, GameCreationEventArgs e) { if (gameLobby.Enabled || gameLoadingLobby.Enabled) return; string channelName = RandomizeChannelName(); Channel gameLoadingChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, e.Password); connectionManager.AddChannel(gameLoadingChannel); gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); gameLoadingChannel.UserAdded += GameLoadingChannel_UserAdded; connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + channelName + " " + e.Password, QueuedMessageType.INSTANT_MESSAGE, 0)); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); gameCreationPanel.Hide(); // update the friends window so it can enable the Invite option pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); } private void GameChannel_InvalidPasswordEntered_LoadedGame(object sender, EventArgs e) { var channel = (Channel)sender; channel.UserAdded -= GameLoadingChannel_UserAdded; channel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_LoadedGame; gameLoadingLobby.Clear(); isJoiningGame = false; } private void GameLoadingChannel_UserAdded(object sender, ChannelUserEventArgs e) { Channel gameLoadingChannel = (Channel)sender; if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { gameLoadingChannel.UserAdded -= GameLoadingChannel_UserAdded; gameLoadingChannel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_LoadedGame; gameLoadingLobby.OnJoined(); isInGameRoom = true; isJoiningGame = false; } } /// /// Generates and returns a random, unused cannel name. /// /// A random channel name based on the currently played game. private string RandomizeChannelName() { int maxTries = 10000; for (int i = 0; i < maxTries; i++) { string channelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID) + "-game" + random.Next(1000000, 9999999); int index = lbGameList.HostedGames.FindIndex(c => ((HostedCnCNetGame)c).ChannelName == channelName); if (index == -1) return channelName; } throw new Exception(string.Format("Could not find a random channel name after {0} retries", maxTries)); } private void Gcw_Cancelled(object sender, EventArgs e) => gameCreationPanel.Hide(); private void TbChatInput_EnterPressed(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbChatInput.Text)) return; IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; currentChatChannel.SendChatMessage(tbChatInput.Text, selectedColor); tbChatInput.Text = string.Empty; } private void SetChatColor() { IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; tbChatInput.TextColor = selectedColor.XnaColor; gameLobby.ChangeChatColor(selectedColor); gameLoadingLobby.ChangeChatColor(selectedColor); UserINISettings.Instance.ChatColor.Value = ddColor.SelectedIndex; } private void DdColor_SelectedIndexChanged(object sender, EventArgs e) { SetChatColor(); UserINISettings.Instance.SaveSettings(); } private void ConnectionManager_Disconnected(object sender, EventArgs e) { btnNewGame.AllowClick = false; btnJoinGame.AllowClick = false; ddCurrentChannel.AllowDropDown = false; tbChatInput.Enabled = false; lbPlayerList.Clear(); lbGameList.ClearGames(); followedGames.Clear(); gameCreationPanel.Hide(); // Switch channel to default if (localGame != null) { int gameIndex = ddCurrentChannel.Items.FindIndex(i => i.Text == localGame.UIName); if (gameIndex > -1) ddCurrentChannel.SelectedIndex = gameIndex; } if (gameCheckCancellation != null) gameCheckCancellation.Cancel(); } private void ConnectionManager_WelcomeMessageReceived(object sender, EventArgs e) { btnNewGame.AllowClick = true; btnJoinGame.AllowClick = true; ddCurrentChannel.AllowDropDown = true; tbChatInput.Enabled = true; Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); cncnetChannel?.Join(); string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); connectionManager.FindChannel(localGameChatChannelName).Join(); string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID); connectionManager.FindChannel(localGameBroadcastChannel).Join(); foreach (CnCNetGame game in gameCollection.GameList) { if (!game.Supported) continue; if (game.InternalName.ToUpper() != localGameID) { if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { connectionManager.FindChannel(game.GameBroadcastChannel).Join(); followedGames.Add(game.InternalName); } } } gameCheckCancellation = new CancellationTokenSource(); CnCNetGameCheck.Instance.InitializeService(gameCheckCancellation); } private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEventArgs e) { foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers) { if (cmdHandler.Handle(e.Sender, e.Message)) return; } Logger.Log("Unhandled private CTCP command: " + e.Message + " from " + e.Sender); } private void HandleGameInviteCommand(string sender, string argumentsString) { // arguments are semicolon-delimited var arguments = argumentsString.Split(';'); // we expect to be given a channel name, a (human-friendly) game name and optionally a password if (arguments.Length < 2 || arguments.Length > 3) return; string channelName = arguments[0]; string gameName = arguments[1]; string password = (arguments.Length == 3) ? arguments[2] : string.Empty; if (!CanReceiveInvitationMessagesFrom(sender)) return; var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName); // also enforce user preference on whether to accept invitations from non-friends // this is kept separate from CanReceiveInvitationMessagesFrom() as we still // want to let the host know that we couldn't receive the invitation if (!string.IsNullOrEmpty(GetJoinGameErrorByIndex(gameIndex)) || (UserINISettings.Instance.AllowGameInvitesFromFriendsOnly && !cncnetUserData.IsFriend(sender))) { // let the host know that we can't accept // note this is not reached for the rejection case connectionManager.SendCustomMessage(new QueuedMessage("PRIVMSG " + sender + " :\u0001" + ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)); return; } // if there's already an outstanding invitation from this user/channel combination, // we don't want to display another // we won't bother telling the host though, since their old invitation is still // available to us var invitationIdentity = new UserChannelPair(sender, channelName); if (invitationIndex.ContainsKey(invitationIdentity)) { return; } var gameInviteChoiceBox = new ChoiceNotificationBox(WindowManager); WindowManager.AddAndInitializeControl(gameInviteChoiceBox); // show the invitation at top left; it will remain until it is acted upon or the target game is closed gameInviteChoiceBox.Show( "GAME INVITATION".L10N("Client:Main:GameInviteTitle"), GetUserTexture(sender), sender, string.Format("Join {0}?".L10N("Client:Main:GameInviteText"), gameName), "Yes".L10N("Client:Main:ButtonYes"), "No".L10N("Client:Main:ButtonNo"), 0); // add the invitation to the index so we can remove it if the target game is closed // also lets us silently ignore new invitations from the same person while this one is still outstanding invitationIndex[invitationIdentity] = new WeakReference(gameInviteChoiceBox); gameInviteChoiceBox.AffirmativeClickedAction = delegate (ChoiceNotificationBox choiceBox) { // if we're currently in a game lobby, first leave that channel if (isInGameRoom) { gameLobby.LeaveGameLobby(); gameLoadingLobby.Clear(); } // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist if (!JoinGameByIndex(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password)) { XNAMessageBox.Show(WindowManager, "Failed to join".L10N("Client:Main:JoinFailedTitle"), string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("Client:Main:JoinFailedText"), sender)); } // clean up the index as this invitation no longer exists invitationIndex.Remove(invitationIdentity); }; gameInviteChoiceBox.NegativeClickedAction = delegate (ChoiceNotificationBox choiceBox) { // clean up the index as this invitation no longer exists invitationIndex.Remove(invitationIdentity); }; sndGameInviteReceived.Play(); } private void HandleGameInvitationFailedNotification(string sender) { if (!CanReceiveInvitationMessagesFrom(sender)) return; if (isInGameRoom && !ProgramConstants.IsInGame) { gameLobby.AddWarning( string.Format(("{0} could not receive your invitation. They might be in game " + "or only accepting invitations from friends. Ensure your game is " + "unlocked and visible in the lobby before trying again.").L10N("Client:Main:InviteNotDelivered"), sender)); } } private void DdCurrentChannel_SelectedIndexChanged(object sender, EventArgs e) { if (currentChatChannel != null) { currentChatChannel.UserAdded -= RefreshPlayerList; currentChatChannel.UserLeft -= RefreshPlayerList; currentChatChannel.UserQuitIRC -= RefreshPlayerList; currentChatChannel.UserKicked -= RefreshPlayerList; currentChatChannel.UserListReceived -= RefreshPlayerList; currentChatChannel.MessageAdded -= CurrentChatChannel_MessageAdded; currentChatChannel.UserGameIndexUpdated -= CurrentChatChannel_UserGameIndexUpdated; if (currentChatChannel.ChannelName != "#cncnet" && currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { // Remove the assigned channels from the users so we don't have ghost users on the PM user list currentChatChannel.Users.DoForAllUsers(user => { connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); }); currentChatChannel.Leave(); } } currentChatChannel = (Channel)ddCurrentChannel.SelectedItem?.Tag; if (currentChatChannel == null) throw new Exception("Current selected chat channel is null. This should not happen."); currentChatChannel.UserAdded += RefreshPlayerList; currentChatChannel.UserLeft += RefreshPlayerList; currentChatChannel.UserQuitIRC += RefreshPlayerList; currentChatChannel.UserKicked += RefreshPlayerList; currentChatChannel.UserListReceived += RefreshPlayerList; currentChatChannel.MessageAdded += CurrentChatChannel_MessageAdded; currentChatChannel.UserGameIndexUpdated += CurrentChatChannel_UserGameIndexUpdated; connectionManager.SetMainChannel(currentChatChannel); lbPlayerList.TopIndex = 0; lbChatMessages.TopIndex = 0; lbChatMessages.Clear(); OnChatMessagesCleared(); currentChatChannel.Messages.ForEach(msg => AddMessageToChat(msg)); RefreshPlayerList(this, EventArgs.Empty); if (currentChatChannel.ChannelName != "#cncnet" && currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { currentChatChannel.Join(); } } private void RefreshPlayerList(object sender, EventArgs e) { string selectedUserName = lbPlayerList.SelectedItem == null ? string.Empty : lbPlayerList.SelectedItem.Text; lbPlayerList.Clear(); // Note: IUserCollection.GetFirst() is not guaranteed to be implemented, unless it is a SortedUserCollection Debug.Assert(currentChatChannel.Users is SortedUserCollection, "Channel 'users' is supposed to be a SortedUserCollection"); var current = currentChatChannel.Users.GetFirst(); while (current != null) { var user = current.Value; user.IRCUser.IsFriend = cncnetUserData.IsFriend(user.IRCUser.Name); user.IRCUser.IsIgnored = cncnetUserData.IsIgnored(user.IRCUser.Ident); lbPlayerList.AddUser(user); current = current.Next; } if (selectedUserName != string.Empty) { lbPlayerList.SelectedIndex = lbPlayerList.Items.FindIndex( i => i.Text == selectedUserName); } } /// /// Refreshes a single user's info on the player list. /// /// User on the current chat channel. private void RefreshPlayerListUser(ChannelUser user) { user.IRCUser.IsFriend = cncnetUserData.IsFriend(user.IRCUser.Name); user.IRCUser.IsIgnored = cncnetUserData.IsIgnored(user.IRCUser.Ident); lbPlayerList.UpdateUserInfo(user); } private void CurrentChatChannel_UserGameIndexUpdated(object sender, ChannelUserEventArgs e) { var ircUser = e.User.IRCUser; var item = lbPlayerList.Items.Find(i => i.Text.StartsWith(ircUser.Name)); if (ircUser.GameID < 0 || ircUser.GameID >= gameCollection.GameList.Count) item.Texture = unknownGameIcon; else item.Texture = gameCollection.GameList[ircUser.GameID].Texture; } private void OnChatMessagesCleared() { ctcpInvalidGameMessageShown = false; ctcpNoTunnelMessageShown = false; ctcpNoTunnelForGamesMessageShown = false; } private void AddMessageToChat(ChatMessage message) { if (!string.IsNullOrEmpty(message.SenderIdent) && cncnetUserData.IsIgnored(message.SenderIdent) && !message.SenderIsAdmin) { lbChatMessages.AddMessage(new ChatMessage(Color.Silver, string.Format("Message blocked from - {0}".L10N("Client:Main:PMBlockedFrom"), message.SenderName))); } else { lbChatMessages.AddMessage(message); } } private void CurrentChatChannel_MessageAdded(object sender, IRCMessageEventArgs e) => AddMessageToChat(e.Message); /// /// Removes a game from the list when the host quits CnCNet or /// leaves the game broadcast channel. /// private void GameBroadcastChannel_UserLeftOrQuit(object sender, UserNameEventArgs e) { int gameIndex = lbGameList.HostedGames.FindIndex(hg => hg.HostName == e.UserName); if (gameIndex > -1) { lbGameList.RemoveGame(gameIndex); // dismiss any outstanding invitations that are no longer valid DismissInvalidInvitations(); } } private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventArgs e) { var channel = (Channel)sender; var channelUser = channel.Users.Find(e.UserName); if (channelUser == null) return; if (localGame != null && channel.ChannelName == localGame.GameBroadcastChannel && !updateDenied && channelUser.IsAdmin && !isInGameRoom && e.Message.StartsWith("UPDATE ") && e.Message.Length > 7) { string version = e.Message.Substring(7); if (version != ProgramConstants.GAME_VERSION) { var updateMessageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Update available".L10N("Client:Main:UpdateAvailableTitle"), "An update is available. Do you want to perform the update now?".L10N("Client:Main:UpdateAvailableText")); updateMessageBox.NoClickedAction = UpdateMessageBox_NoClicked; updateMessageBox.YesClickedAction = UpdateMessageBox_YesClicked; } } if (!e.Message.StartsWith("GAME ")) return; string msg = e.Message.Substring(5); // Cut out GAME part string[] splitMessage = msg.Split(new char[] { ';' }); if (splitMessage.Length != 14) { Logger.Log("Ignoring CTCP game message because of an invalid amount of parameters."); // Remind users that the network is good but the client is outdated or newer if (lbGameList.Items.Count == 0 && lbGameList.HostedGames.Count == 0 && !ctcpInvalidGameMessageShown) { ctcpInvalidGameMessageShown = true; string message = ("There are no games listed but you are indeed connected. The client did receive a game message but can't add it to the list because the message is invalid. " + "You can ignore this prompt if there are games listed later. " + "Otherwise, this usually means that your client is outdated, or, in a rare case, newer than others. Please check for updates.").L10N("Client:Main:InvalidGameMessage"); lbChatMessages.AddMessage(new ChatMessage(Color.Gray, message)); } return; } try { string revision = splitMessage[0]; if (revision != ProgramConstants.CNCNET_PROTOCOL_REVISION) return; string gameVersion = splitMessage[1]; int maxPlayers = Conversions.IntFromString(splitMessage[2], 0); string gameRoomChannelName = splitMessage[3]; string gameRoomDisplayName = splitMessage[4]; bool locked = Conversions.BooleanFromString(splitMessage[5].Substring(0, 1), true); bool isCustomPassword = Conversions.BooleanFromString(splitMessage[5].Substring(1, 1), false); bool isClosed = Conversions.BooleanFromString(splitMessage[5].Substring(2, 1), true); bool isLoadedGame = Conversions.BooleanFromString(splitMessage[5].Substring(3, 1), false); bool isLadder = Conversions.BooleanFromString(splitMessage[5].Substring(4, 1), false); string[] players = splitMessage[6].Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); List playerNames = players.ToList(); string mapName = splitMessage[7]; string gameMode = splitMessage[8]; string[] tunnelAddressAndPort = splitMessage[9].Split(':'); string tunnelAddress = tunnelAddressAndPort[0]; int tunnelPort = int.Parse(tunnelAddressAndPort[1]); string loadedGameId = splitMessage[10]; int skillLevel = int.Parse(splitMessage[11]); string mapHash = splitMessage[12]; int[] gameOptionValues = null; // Games with different versions may have different option counts, so ignore if (gameVersion == ProgramConstants.GAME_VERSION && channel.ChannelName == localGame?.GameBroadcastChannel) { var broadcastableSettings = gameLobby.GetBroadcastableSettings(); if (broadcastableSettings.Count == 0) { gameOptionValues = null; } else if (!string.IsNullOrEmpty(splitMessage[13])) { gameOptionValues = new int[broadcastableSettings.Count]; string[] allValueStrings = splitMessage[13].Split(','); int checkboxCount = gameLobby.CheckBoxes.Count(cb => cb.BroadcastToLobby); int packedCheckboxCount = (checkboxCount + 31) / 32; // packed checkbox values if (checkboxCount > 0 && allValueStrings.Length >= packedCheckboxCount) { int[] packedCheckboxes = new int[packedCheckboxCount]; for (int i = 0; i < packedCheckboxCount; i++) packedCheckboxes[i] = int.Parse(allValueStrings[i]); for (int i = 0; i < checkboxCount; i++) { int packedIndex = i / 32; int bitIndex = i % 32; gameOptionValues[i] = (packedCheckboxes[packedIndex] & (1 << bitIndex)) != 0 ? 1 : 0; } } // dropdown indices int dropdownCount = gameLobby.DropDowns.Count(dd => dd.BroadcastToLobby); if (dropdownCount > 0) { int count = Math.Min(allValueStrings.Length - packedCheckboxCount, dropdownCount); for (int i = 0; i < count; i++) gameOptionValues[checkboxCount + i] = int.Parse(allValueStrings[packedCheckboxCount + i]); } } } CnCNetGame cncnetGame = gameCollection.GameList.Find(g => g.GameBroadcastChannel == channel.ChannelName); if (cncnetGame == null) return; // Find the tunnel server specified in the game message if (tunnelHandler.Tunnels.Count == 0) { Logger.Log("Ignoring CTCP game message because there are no tunnels at all. Available tunnel count: 0. Is the connection to CnCNet HTTP service broken?"); // Remind users that the game is ignored because of no tunnel if (lbGameList.Items.Count == 0 && lbGameList.HostedGames.Count == 0 && !ctcpNoTunnelMessageShown) { ctcpNoTunnelMessageShown = true; string message = ("There are no games listed. The client did receive a valid game message but can't add it to the list because there are no available tunnels. " + "You can ignore this prompt if there are games listed later. Otherwise, it might indicate a network problem to CnCNet HTTP service.").L10N("Client:Main:NoTunnels"); lbChatMessages.AddMessage(new ChatMessage(Color.Gray, message)); } return; } CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); if (tunnel == null) { Logger.Log(string.Format("Ignoring CTCP game message because the specified tunnel {0}:{1} is not available. Available tunnel count: {2}", tunnelAddress, tunnelPort, tunnelHandler.Tunnels.Count)); // Remind users that the game is ignored because of no specified tunnel if (lbGameList.Items.Count == 0 && lbGameList.HostedGames.Count == 0 && !ctcpNoTunnelForGamesMessageShown) { ctcpNoTunnelForGamesMessageShown = true; string message = string.Format(("There are no games listed. The client did receive a valid game message but can't add it to the list because the specified tunnel is not available. " + "You can ignore this prompt if there are games listed later. Otherwise, please contact support at {0}.").L10N("Client:Main:NoTunnelForGames"), ClientConfiguration.Instance.LongSupportURL); lbChatMessages.AddMessage(new ChatMessage(Color.Gray, message)); } return; } HostedCnCNetGame game = new HostedCnCNetGame(gameRoomChannelName, revision, gameVersion, maxPlayers, gameRoomDisplayName, isCustomPassword, true, players, e.UserName, mapName, gameMode, mapHash); game.IsLoadedGame = isLoadedGame; game.MatchID = loadedGameId; game.LastRefreshTime = DateTime.Now; game.IsLadder = isLadder; game.Game = cncnetGame; game.Locked = locked || (game.IsLoadedGame && !game.Players.Contains(ProgramConstants.PLAYERNAME)); game.Incompatible = cncnetGame == localGame && game.GameVersion != ProgramConstants.GAME_VERSION; game.TunnelServer = tunnel; game.SkillLevel = skillLevel; game.BroadcastedGameOptionValues = gameOptionValues; if (isClosed) { int index = lbGameList.HostedGames.FindIndex(hg => hg.HostName == e.UserName); if (index > -1) { lbGameList.RemoveGame(index); // dismiss any outstanding invitations that are no longer valid DismissInvalidInvitations(); } return; } // Seek for the game in the internal game list based on the name of its host; // if found, then refresh that game's information, otherwise add as new game int gameIndex = lbGameList.HostedGames.FindIndex(hg => hg.HostName == e.UserName); if (gameIndex > -1) { lbGameList.HostedGames[gameIndex] = game; } else { if (UserINISettings.Instance.PlaySoundOnGameHosted && cncnetGame.InternalName == localGameID.ToLower() && !ProgramConstants.IsInGame && !game.Locked) { SoundPlayer.Play(sndGameCreated); } lbGameList.AddGame(game); } SortAndRefreshHostedGames(); } catch (Exception ex) { Logger.Log("Game parsing error: " + ex.ToString()); } } private void UpdateMessageBox_YesClicked(XNAMessageBox messageBox) => UpdateCheck?.Invoke(this, EventArgs.Empty); private void UpdateMessageBox_NoClicked(XNAMessageBox messageBox) => updateDenied = true; private void BtnLogout_LeftClick(object sender, EventArgs e) { if (isInGameRoom) { topBar.SwitchToPrimary(); return; } if (connectionManager.IsConnected && !UserINISettings.Instance.PersistentMode) { connectionManager.Disconnect(); } topBar.SwitchToPrimary(); } public void SwitchOn() { Enable(); if (!connectionManager.IsConnected && !connectionManager.IsAttemptingConnection) { loginWindow.Enable(); loginWindow.LoadSettings(); } SetLogOutButtonText(); } public void SwitchOff() => Disable(); public string GetSwitchName() => "CnCNet Lobby".L10N("Client:Main:CnCNetLobby"); private bool CanReceiveInvitationMessagesFrom(string username) { IRCUser iu = connectionManager.UserList.Find(u => u.Name == username); // We don't accept invitation messages from people who we don't share any channels with if (iu == null) { return false; } // Invitation messages from users we've blocked are not wanted if (cncnetUserData.IsIgnored(iu.Ident)) { return false; } return true; } private Texture2D GetUserTexture(string username) { Texture2D senderGameIcon = unknownGameIcon; IRCUser iu = connectionManager.UserList.Find(u => u.Name == username); if (iu != null && iu.GameID >= 0 && iu.GameID < gameCollection.GameList.Count) { senderGameIcon = gameCollection.GameList[iu.GameID].Texture; } return senderGameIcon; } private void DismissInvalidInvitations() { var toDismiss = new List(); foreach (KeyValuePair invitation in invitationIndex) { var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).HostName == invitation.Key.Item1 && ((HostedCnCNetGame)hg).ChannelName == invitation.Key.Item2); if (gameIndex == -1) { toDismiss.Add(invitation.Key); } } foreach (UserChannelPair invitationIdentity in toDismiss) { DismissInvitation(invitationIdentity); } } private void DismissInvitation(UserChannelPair invitationIdentity) { if (invitationIndex.ContainsKey(invitationIdentity)) { var invitationNotification = invitationIndex[invitationIdentity].Target as ChoiceNotificationBox; if (invitationNotification != null) { WindowManager.RemoveControl(invitationNotification); } invitationIndex.Remove(invitationIdentity); } } /// /// Attempts to find a hosted game that the specified user is in /// /// The user to find a game for. /// private HostedCnCNetGame GetHostedGameForUser(IRCUser user) { return lbGameList.HostedGames.Select(g => (HostedCnCNetGame)g).FirstOrDefault(g => g.Players.Contains(user.Name)); } /// /// Joins a specified user's game depending on whether or not /// they are currently in one. /// /// The user to join. /// The message view/list to write error messages to. private void JoinUser(IRCUser user, IMessageView messageView) { if (user == null) { // can happen if a user is selected while offline messageView.AddMessage(new ChatMessage(Color.White, "User is not currently available!".L10N("Client:Main:UserNotAvailable"))); return; } var game = GetHostedGameForUser(user); if (game == null) { messageView.AddMessage(new ChatMessage(Color.White, string.Format("{0} is not in a game!".L10N("Client:Main:UserNotInGame"), user.Name))); return; } int gameIndex = lbGameList.Items.FindIndex(item => item.Tag == game); if (gameIndex >= 0) { lbGameList.SelectedIndex = gameIndex; lbGameList.ScrollToSelectedElement(); } JoinGame(game, string.Empty, messageView); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLoginWindow.cs ================================================ using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using ClientGUI; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Multiplayer.CnCNet { class CnCNetLoginWindow : XNAWindow { public CnCNetLoginWindow(WindowManager windowManager) : base(windowManager) { } XNALabel lblConnectToCnCNet; XNATextBox tbPlayerName; XNALabel lblPlayerName; XNAClientCheckBox chkRememberMe; XNAClientCheckBox chkPersistentMode; XNAClientCheckBox chkAutoConnect; XNAClientButton btnConnect; XNAClientButton btnCancel; public event EventHandler Cancelled; public event EventHandler Connect; public override void Initialize() { Name = "CnCNetLoginWindow"; ClientRectangle = new Rectangle(0, 0, 300, 220); BackgroundTexture = AssetLoader.LoadTextureUncached("logindialogbg.png"); lblConnectToCnCNet = new XNALabel(WindowManager); lblConnectToCnCNet.Name = "lblConnectToCnCNet"; lblConnectToCnCNet.FontIndex = 1; lblConnectToCnCNet.Text = "CONNECT TO CNCNET".L10N("Client:Main:ConnectToCncNet"); AddChild(lblConnectToCnCNet); lblConnectToCnCNet.CenterOnParent(); lblConnectToCnCNet.ClientRectangle = new Rectangle( lblConnectToCnCNet.X, 12, lblConnectToCnCNet.Width, lblConnectToCnCNet.Height); tbPlayerName = new XNATextBox(WindowManager); tbPlayerName.Name = "tbPlayerName"; tbPlayerName.ClientRectangle = new Rectangle(Width - 132, 50, 120, 19); tbPlayerName.MaximumTextLength = ClientConfiguration.Instance.MaxNameLength; tbPlayerName.IMEDisabled = true; string defgame = ClientConfiguration.Instance.LocalGame; lblPlayerName = new XNALabel(WindowManager); lblPlayerName.Name = "lblPlayerName"; lblPlayerName.FontIndex = 1; lblPlayerName.Text = "PLAYER NAME:".L10N("Client:Main:PlayerName"); lblPlayerName.ClientRectangle = new Rectangle(12, tbPlayerName.Y + 1, lblPlayerName.Width, lblPlayerName.Height); chkRememberMe = new XNAClientCheckBox(WindowManager); chkRememberMe.Name = "chkRememberMe"; chkRememberMe.ClientRectangle = new Rectangle(12, tbPlayerName.Bottom + 12, 0, 0); chkRememberMe.Text = "Remember me".L10N("Client:Main:RememberMe"); chkRememberMe.TextPadding = 7; chkRememberMe.CheckedChanged += ChkRememberMe_CheckedChanged; chkPersistentMode = new XNAClientCheckBox(WindowManager); chkPersistentMode.Name = "chkPersistentMode"; chkPersistentMode.ClientRectangle = new Rectangle(12, chkRememberMe.Bottom + 30, 0, 0); chkPersistentMode.Text = "Stay connected outside of the CnCNet lobby".L10N("Client:Main:StayConnect"); chkPersistentMode.TextPadding = chkRememberMe.TextPadding; chkPersistentMode.CheckedChanged += ChkPersistentMode_CheckedChanged; chkAutoConnect = new XNAClientCheckBox(WindowManager); chkAutoConnect.Name = "chkAutoConnect"; chkAutoConnect.ClientRectangle = new Rectangle(12, chkPersistentMode.Bottom + 30, 0, 0); chkAutoConnect.Text = "Connect automatically on client startup".L10N("Client:Main:AutoConnect"); chkAutoConnect.TextPadding = chkRememberMe.TextPadding; chkAutoConnect.AllowChecking = false; btnConnect = new XNAClientButton(WindowManager); btnConnect.Name = "btnConnect"; btnConnect.ClientRectangle = new Rectangle(12, Height - 35, 110, 23); btnConnect.Text = "Connect".L10N("Client:Main:ButtonConnect"); btnConnect.LeftClick += BtnConnect_LeftClick; btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = "btnCancel"; btnCancel.ClientRectangle = new Rectangle(Width - 122, btnConnect.Y, 110, 23); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(tbPlayerName); AddChild(lblPlayerName); AddChild(chkRememberMe); AddChild(chkPersistentMode); AddChild(chkAutoConnect); AddChild(btnConnect); AddChild(btnCancel); base.Initialize(); CenterOnParent(); UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved; } private void Instance_SettingsSaved(object sender, EventArgs e) { tbPlayerName.Text = UserINISettings.Instance.PlayerName; } private void BtnCancel_LeftClick(object sender, EventArgs e) { Cancelled?.Invoke(this, EventArgs.Empty); } private void ChkRememberMe_CheckedChanged(object sender, EventArgs e) { CheckAutoConnectAllowance(); } private void ChkPersistentMode_CheckedChanged(object sender, EventArgs e) { CheckAutoConnectAllowance(); } private void CheckAutoConnectAllowance() { chkAutoConnect.AllowChecking = chkPersistentMode.Checked && chkRememberMe.Checked; if (!chkAutoConnect.AllowChecking) chkAutoConnect.Checked = false; } private void BtnConnect_LeftClick(object sender, EventArgs e) { NameValidationError validationError = NameValidator.IsNameValid(tbPlayerName.Text, out string errorMessage); if (validationError != NameValidationError.None) { XNAMessageBox.Show(WindowManager, "Invalid Player Name".L10N("Client:Main:InvalidPlayerName"), errorMessage); return; } ProgramConstants.PLAYERNAME = tbPlayerName.Text; UserINISettings.Instance.SkipConnectDialog.Value = chkRememberMe.Checked; UserINISettings.Instance.PersistentMode.Value = chkPersistentMode.Checked; UserINISettings.Instance.AutomaticCnCNetLogin.Value = chkAutoConnect.Checked; UserINISettings.Instance.PlayerName.Value = ProgramConstants.PLAYERNAME; UserINISettings.Instance.SaveSettings(); Connect?.Invoke(this, EventArgs.Empty); } public void LoadSettings() { chkAutoConnect.Checked = UserINISettings.Instance.AutomaticCnCNetLogin; chkPersistentMode.Checked = UserINISettings.Instance.PersistentMode; chkRememberMe.Checked = UserINISettings.Instance.SkipConnectDialog; tbPlayerName.Text = UserINISettings.Instance.PlayerName; if (chkRememberMe.Checked) BtnConnect_LeftClick(this, EventArgs.Empty); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationEventArgs.cs ================================================ using DTAClient.Domain.Multiplayer.CnCNet; using System; namespace DTAClient.DXGUI.Multiplayer.CnCNet { class GameCreationEventArgs : EventArgs { public GameCreationEventArgs(string roomName, int maxPlayers, string password, CnCNetTunnel tunnel, int skillLevel) { GameRoomName = roomName; MaxPlayers = maxPlayers; Password = password; Tunnel = tunnel; SkillLevel = skillLevel; } public string GameRoomName { get; private set; } public int MaxPlayers { get; private set; } public string Password { get; private set; } public CnCNetTunnel Tunnel { get; private set; } public int SkillLevel { get; private set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs ================================================ using ClientCore; using ClientGUI; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.IO; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A window that allows the user to host a new game on CnCNet. /// class GameCreationWindow : XNAWindow { public GameCreationWindow(WindowManager windowManager, TunnelHandler tunnelHandler) : base(windowManager) { this.tunnelHandler = tunnelHandler; } public event EventHandler Cancelled; public event EventHandler GameCreated; public event EventHandler LoadedGameCreated; private XNATextBox tbGameName; private XNAClientDropDown ddMaxPlayers; private XNAClientDropDown ddSkillLevel; private XNATextBox tbPassword; private XNALabel lblRoomName; private XNALabel lblMaxPlayers; private XNALabel lblSkillLevel; private XNALabel lblPassword; private XNALabel lblTunnelServer; private TunnelListBox lbTunnelList; private XNAClientButton btnCreateGame; private XNAClientButton btnCancel; private XNAClientButton btnLoadMPGame; private XNAClientButton btnDisplayAdvancedOptions; private TunnelHandler tunnelHandler; private string[] SkillLevelOptions; public override void Initialize() { lbTunnelList = new TunnelListBox(WindowManager, tunnelHandler); lbTunnelList.Name = nameof(lbTunnelList); SkillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(','); Name = "GameCreationWindow"; Width = lbTunnelList.Width + UIDesignConstants.EMPTY_SPACE_SIDES * 2 + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN * 2; BackgroundTexture = AssetLoader.LoadTexture("gamecreationoptionsbg.png"); tbGameName = new XNATextBox(WindowManager); tbGameName.Name = nameof(tbGameName); tbGameName.MaximumTextLength = 23; tbGameName.ClientRectangle = new Rectangle(Width - 150 - UIDesignConstants.EMPTY_SPACE_SIDES - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, UIDesignConstants.EMPTY_SPACE_TOP + UIDesignConstants.CONTROL_VERTICAL_MARGIN, 150, 21); tbGameName.Text = string.Format("{0}'s Game", ProgramConstants.PLAYERNAME); lblRoomName = new XNALabel(WindowManager); lblRoomName.Name = nameof(lblRoomName); lblRoomName.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, tbGameName.Y + 1, 0, 0); lblRoomName.Text = "Game room name:".L10N("Client:Main:GameRoomName"); ddMaxPlayers = new XNAClientDropDown(WindowManager); ddMaxPlayers.Name = nameof(ddMaxPlayers); ddMaxPlayers.ClientRectangle = new Rectangle(tbGameName.X, tbGameName.Bottom + 20, tbGameName.Width, 21); for (int i = 8; i > 1; i--) ddMaxPlayers.AddItem(i.ToString()); ddMaxPlayers.SelectedIndex = 0; lblMaxPlayers = new XNALabel(WindowManager); lblMaxPlayers.Name = nameof(lblMaxPlayers); lblMaxPlayers.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, ddMaxPlayers.Y + 1, 0, 0); lblMaxPlayers.Text = "Maximum number of players:".L10N("Client:Main:GameMaxPlayerCount"); // Skill Level selector ddSkillLevel = new XNAClientDropDown(WindowManager); ddSkillLevel.Name = nameof(ddSkillLevel); ddSkillLevel.ClientRectangle = new Rectangle(tbGameName.X, ddMaxPlayers.Bottom + 20, tbGameName.Width, 21); for (int i = 0; i < SkillLevelOptions.Length; i++) { string skillLevel = SkillLevelOptions[i]; string localizedSkillLevel = skillLevel.L10N($"INI:ClientDefinitions:SkillLevel:{i}"); ddSkillLevel.AddItem(localizedSkillLevel); } ddSkillLevel.SelectedIndex = ClientConfiguration.Instance.DefaultSkillLevelIndex; lblSkillLevel = new XNALabel(WindowManager); lblSkillLevel.Name = nameof(lblSkillLevel); lblSkillLevel.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, ddSkillLevel.Y + 1, 0, 0); lblSkillLevel.Text = "Select preferred skill level of players:".L10N("Client:Main:SelectSkillLevel"); tbPassword = new XNATextBox(WindowManager); tbPassword.Name = nameof(tbPassword); tbPassword.MaximumTextLength = 20; tbPassword.ClientRectangle = new Rectangle(tbGameName.X, ddSkillLevel.Bottom + 20, tbGameName.Width, 21); lblPassword = new XNALabel(WindowManager); lblPassword.Name = nameof(lblPassword); lblPassword.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, tbPassword.Y + 1, 0, 0); lblPassword.Text = "Password (leave blank for none):".L10N("Client:Main:PasswordTextBlankForNone"); btnDisplayAdvancedOptions = new XNAClientButton(WindowManager); btnDisplayAdvancedOptions.Name = nameof(btnDisplayAdvancedOptions); btnDisplayAdvancedOptions.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, lblPassword.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnDisplayAdvancedOptions.Text = "Advanced Options".L10N("Client:Main:AdvancedOptions"); btnDisplayAdvancedOptions.LeftClick += BtnDisplayAdvancedOptions_LeftClick; lblTunnelServer = new XNALabel(WindowManager); lblTunnelServer.Name = nameof(lblTunnelServer); lblTunnelServer.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, lblPassword.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 4, 0, 0); lblTunnelServer.Text = "Tunnel server:".L10N("Client:Main:TunnelServer"); lblTunnelServer.Enabled = false; lblTunnelServer.Visible = false; lbTunnelList.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; lbTunnelList.Y = lblTunnelServer.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN; lbTunnelList.Disable(); lbTunnelList.ListRefreshed += LbTunnelList_ListRefreshed; btnCreateGame = new XNAClientButton(WindowManager); btnCreateGame.Name = nameof(btnCreateGame); btnCreateGame.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, btnDisplayAdvancedOptions.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnCreateGame.Text = "Create Game".L10N("Client:Main:CreateGame"); btnCreateGame.LeftClick += BtnCreateGame_LeftClick; btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.ClientRectangle = new Rectangle(Width - UIDesignConstants.BUTTON_WIDTH_133 - UIDesignConstants.EMPTY_SPACE_SIDES - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, btnCreateGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; int btnLoadMPGameX = btnCreateGame.Right + (btnCancel.X - btnCreateGame.Right) / 2 - UIDesignConstants.BUTTON_WIDTH_133 / 2; btnLoadMPGame = new XNAClientButton(WindowManager); btnLoadMPGame.Name = nameof(btnLoadMPGame); btnLoadMPGame.ClientRectangle = new Rectangle(btnLoadMPGameX, btnCreateGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLoadMPGame.Text = "Load Game".L10N("Client:Main:LoadGame"); btnLoadMPGame.LeftClick += BtnLoadMPGame_LeftClick; AddChild(tbGameName); AddChild(lblRoomName); AddChild(ddMaxPlayers); AddChild(lblMaxPlayers); AddChild(ddSkillLevel); AddChild(lblSkillLevel); AddChild(tbPassword); AddChild(lblPassword); AddChild(btnDisplayAdvancedOptions); AddChild(lblTunnelServer); AddChild(lbTunnelList); AddChild(btnCreateGame); if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) AddChild(btnLoadMPGame); AddChild(btnCancel); Height = btnCreateGame.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM; base.Initialize(); CenterOnParent(); UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved; if (UserINISettings.Instance.AlwaysDisplayTunnelList) BtnDisplayAdvancedOptions_LeftClick(this, EventArgs.Empty); } private void LbTunnelList_ListRefreshed(object sender, EventArgs e) { if (lbTunnelList.ItemCount == 0) { btnCreateGame.AllowClick = false; btnLoadMPGame.AllowClick = false; } else { btnCreateGame.AllowClick = true; btnLoadMPGame.AllowClick = AllowLoadingGame(); } } private void Instance_SettingsSaved(object sender, EventArgs e) { tbGameName.Text = string.Format("{0}'s Game", UserINISettings.Instance.PlayerName.Value); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Cancelled?.Invoke(this, EventArgs.Empty); } private void BtnLoadMPGame_LeftClick(object sender, EventArgs e) { string gameName = NameValidator.GetSanitizedGameName(tbGameName.Text); NameValidationError validationError = NameValidator.IsGameNameValid(gameName, out string errorMessage); if (validationError != NameValidationError.None) { XNAMessageBox.Show(WindowManager, "Invalid game name".L10N("Client:Main:InvalidGameName"), errorMessage); return; } if (!lbTunnelList.IsValidIndexSelected()) return; IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); string password = Utilities.CalculateSHA1ForString( spawnSGIni.GetStringValue("Settings", "GameID", string.Empty)).Substring(0, 10); GameCreationEventArgs ea = new GameCreationEventArgs(gameName, spawnSGIni.GetIntValue("Settings", "PlayerCount", 2), password, tunnelHandler.Tunnels[lbTunnelList.SelectedIndex], ddSkillLevel.SelectedIndex); LoadedGameCreated?.Invoke(this, ea); } private void BtnCreateGame_LeftClick(object sender, EventArgs e) { string gameName = NameValidator.GetSanitizedGameName(tbGameName.Text); NameValidationError validationError = NameValidator.IsGameNameValid(gameName, out string errorMessage); if (validationError != NameValidationError.None) { XNAMessageBox.Show(WindowManager, "Invalid game name".L10N("Client:Main:InvalidGameName"), errorMessage); return; } if (!lbTunnelList.IsValidIndexSelected()) { return; } GameCreated?.Invoke(this, new GameCreationEventArgs(gameName,int.Parse(ddMaxPlayers.SelectedItem.Text), tbPassword.Text,tunnelHandler.Tunnels[lbTunnelList.SelectedIndex], ddSkillLevel.SelectedIndex) ); } private void BtnDisplayAdvancedOptions_LeftClick(object sender, EventArgs e) { Name = "GameCreationWindow_Advanced"; btnCreateGame.ClientRectangle = new Rectangle(btnCreateGame.X, lbTunnelList.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3, btnCreateGame.Width, btnCreateGame.Height); btnCancel.ClientRectangle = new Rectangle(btnCancel.X, btnCreateGame.Y, btnCancel.Width, btnCancel.Height); btnLoadMPGame.ClientRectangle = new Rectangle(btnLoadMPGame.X, btnCreateGame.Y, btnLoadMPGame.Width, btnLoadMPGame.Height); Height = btnCreateGame.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM; lblTunnelServer.Enable(); lbTunnelList.Enable(); btnDisplayAdvancedOptions.Disable(); SetAttributesFromIni(); CenterOnParent(); } public void Refresh() { btnLoadMPGame.AllowClick = AllowLoadingGame(); } private bool AllowLoadingGame() { FileInfo savedGameSpawnIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI); if (!savedGameSpawnIniFile.Exists) return false; IniFile iniFile = new IniFile(savedGameSpawnIniFile.FullName); if (iniFile.GetStringValue("Settings", "Name", string.Empty) != ProgramConstants.PLAYERNAME) return false; if (!iniFile.GetBooleanValue("Settings", "Host", false)) return false; return true; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using ClientCore; using ClientCore.Extensions; using ClientGUI; using DTAClient.DXGUI.Generic; using DTAClient.Online; using DTAClient.Online.EventArguments; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using TextCopy; namespace DTAClient.DXGUI.Multiplayer.CnCNet { public class GlobalContextMenu : XNAContextMenu { private readonly string PRIVATE_MESSAGE = "Private Message".L10N("Client:Main:PrivateMessage"); private readonly string ADD_FRIEND = "Add Friend".L10N("Client:Main:AddFriend"); private readonly string REMOVE_FRIEND = "Remove Friend".L10N("Client:Main:RemoveFriend"); private readonly string BLOCK = "Block".L10N("Client:Main:Block"); private readonly string UNBLOCK = "Unblock".L10N("Client:Main:Unblock"); private readonly string INVITE = "Invite".L10N("Client:Main:Invite"); private readonly string JOIN = "Join".L10N("Client:Main:Join"); private readonly string COPY_LINK = "Copy Link".L10N("Client:Main:CopyLink"); private readonly string OPEN_LINK = "Open Link".L10N("Client:Main:OpenLink"); private readonly int SHORT_LINK_MINIMAL_LENGTH = 40; private readonly int SHORT_LINK_PREFIX_LENGTH = 30; private readonly int SHORT_LINK_SUFFIX_LENGTH = 5; private readonly Rectangle STD_SIZE = new Rectangle(0, 0, 150, 2); private readonly Rectangle LNK_SIZE = new Rectangle(0, 0, 300, 2); private readonly CnCNetUserData cncnetUserData; private readonly PrivateMessagingWindow pmWindow; private XNAContextMenuItem privateMessageItem; private XNAContextMenuItem toggleFriendItem; private XNAContextMenuItem toggleIgnoreItem; private XNAContextMenuItem invitePlayerItem; private XNAContextMenuItem joinPlayerItem; protected readonly CnCNetManager connectionManager; protected GlobalContextMenuData contextMenuData; public EventHandler JoinEvent; private IReadOnlyList DefaultMenuItems = []; public GlobalContextMenu( WindowManager windowManager, CnCNetManager connectionManager, CnCNetUserData cncnetUserData, PrivateMessagingWindow pmWindow ) : base(windowManager) { this.connectionManager = connectionManager; this.cncnetUserData = cncnetUserData; this.pmWindow = pmWindow; Name = nameof(GlobalContextMenu); ClientRectangle = STD_SIZE; Enabled = false; Visible = false; } public override void Initialize() { privateMessageItem = new XNAContextMenuItem() { Text = PRIVATE_MESSAGE, SelectAction = () => pmWindow.InitPM(GetIrcUser().Name) }; toggleFriendItem = new XNAContextMenuItem() { Text = ADD_FRIEND, SelectAction = () => cncnetUserData.ToggleFriend(GetIrcUser().Name) }; toggleIgnoreItem = new XNAContextMenuItem() { Text = BLOCK, SelectAction = () => GetIrcUserIdent(cncnetUserData.ToggleIgnoreUser) }; invitePlayerItem = new XNAContextMenuItem() { Text = INVITE, SelectAction = Invite }; joinPlayerItem = new XNAContextMenuItem() { Text = JOIN, SelectAction = () => JoinEvent?.Invoke(this, new JoinUserEventArgs(GetIrcUser())) }; DefaultMenuItems = [privateMessageItem, toggleFriendItem, toggleIgnoreItem, invitePlayerItem, joinPlayerItem]; foreach (var item in DefaultMenuItems) AddItem(item); } private void Invite() { // note it's assumed that if the channel name is specified, the game name must be also if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame) { return; } string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword)) { messageBody += ";" + contextMenuData.inviteChannelPassword; } connectionManager.SendCustomMessage(new QueuedMessage( "PRIVMSG " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0 )); } private void UpdateButtons() { UpdatePlayerBasedButtons(); UpdateMessageBasedButtons(); } private void UpdatePlayerBasedButtons() { var ircUser = GetIrcUser(); var isOnline = ircUser != null && connectionManager.UserList.Any(u => u.Name == ircUser.Name); var isAdmin = contextMenuData.ChannelUser?.IsAdmin ?? false; toggleFriendItem.Visible = ircUser != null; privateMessageItem.Visible = ircUser != null && isOnline; toggleIgnoreItem.Visible = ircUser != null; invitePlayerItem.Visible = ircUser != null && isOnline && !string.IsNullOrEmpty(contextMenuData.inviteChannelName); joinPlayerItem.Visible = ircUser != null && !contextMenuData.PreventJoinGame && isOnline; toggleIgnoreItem.Selectable = !isAdmin; if (ircUser == null) return; toggleFriendItem.Text = cncnetUserData.IsFriend(ircUser.Name) ? REMOVE_FRIEND : ADD_FRIEND; toggleIgnoreItem.Text = cncnetUserData.IsIgnored(ircUser.Ident) ? UNBLOCK : BLOCK; } private void UpdateMessageBasedButtons() { Items = DefaultMenuItems.ToList(); var links = contextMenuData?.ChatMessage?.Message?.GetLinks(); if (links == null) { ClientRectangle = STD_SIZE; return; } ClientRectangle = LNK_SIZE; foreach (string link in links) { // Shorten the links if it's too long string linkToDisplay = link; if (link.Length > SHORT_LINK_MINIMAL_LENGTH) linkToDisplay = link[..SHORT_LINK_PREFIX_LENGTH] + "..." + link[^SHORT_LINK_SUFFIX_LENGTH..]; if (Items.Where(item => item.Text.Contains(linkToDisplay)).ToList().Count > 0) continue; var copyLinkItem = new XNAContextMenuItem() { Text = $"{COPY_LINK} {linkToDisplay}", SelectAction = () => CopyLink(link) }; var openLinkItem = new XNAContextMenuItem() { Text = $"{OPEN_LINK} {linkToDisplay}", SelectAction = () => URLHandler.OpenLink(WindowManager, link) }; AddItem(openLinkItem); AddItem(copyLinkItem); } } private void CopyLink(string link) { try { ClipboardService.SetText(link); } catch (Exception) { XNAMessageBox.Show(WindowManager, "Error".L10N("Client:Main:Error"), "Unable to copy link".L10N("Client:Main:ClipboardCopyLinkFailed")); } } private void GetIrcUserIdent(Action callback) { var ircUser = GetIrcUser(); if (!string.IsNullOrEmpty(ircUser.Ident)) { callback.Invoke(ircUser.Ident); return; } void WhoIsReply(object sender, WhoEventArgs whoEventargs) { ircUser.Ident = whoEventargs.Ident; callback.Invoke(whoEventargs.Ident); connectionManager.WhoReplyReceived -= WhoIsReply; } connectionManager.WhoReplyReceived += WhoIsReply; connectionManager.SendWhoIsMessage(ircUser.Name); } private IRCUser GetIrcUser() { if (contextMenuData.IrcUser != null) return contextMenuData.IrcUser; if (contextMenuData.ChannelUser?.IRCUser != null) return contextMenuData.ChannelUser.IRCUser; if (!string.IsNullOrEmpty(contextMenuData.PlayerName)) return connectionManager.UserList.Find(u => u.Name == contextMenuData.PlayerName); if (!string.IsNullOrEmpty(contextMenuData.ChatMessage?.SenderName)) return connectionManager.UserList.Find(u => u.Name == contextMenuData.ChatMessage.SenderName); return null; } public void Show(string playerName, Point cursorPoint) { Show(new GlobalContextMenuData { PlayerName = playerName }, cursorPoint); } public void Show(IRCUser ircUser, Point cursorPoint) { Show(new GlobalContextMenuData { IrcUser = ircUser }, cursorPoint); } public void Show(ChannelUser channelUser, Point cursorPoint) { Show(new GlobalContextMenuData { ChannelUser = channelUser }, cursorPoint); } public void Show(ChatMessage chatMessage, Point cursorPoint) { Show(new GlobalContextMenuData() { ChatMessage = chatMessage }, cursorPoint); } public void Show(GlobalContextMenuData data, Point cursorPoint) { Disable(); contextMenuData = data; UpdateButtons(); if (!Items.Any(i => i.Visible)) return; Open(cursorPoint); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenuData.cs ================================================ using DTAClient.Online; namespace DTAClient.DXGUI.Multiplayer.CnCNet { public class GlobalContextMenuData { /// /// The ChannelUser to show the menu for. /// public ChannelUser ChannelUser { get; set; } /// /// The ChatMessage to show the menu for. /// public ChatMessage ChatMessage { get; set; } /// /// The IRCUser to show the menu for. /// public IRCUser IrcUser { get; set; } /// /// The player to show the menu for. This is used to determine the IRCUser internally. /// public string PlayerName { get; set; } /// /// The invite properties are used for the Invite option in the menu. /// public string inviteChannelName { get; set; } public string inviteGameName { get; set; } public string inviteChannelPassword { get; set; } /// /// Prevent the Join option from showing in the menu. /// public bool PreventJoinGame { get; set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/LoadOrSaveGameOptionPresetWindow.cs ================================================ using System; using System.Linq; using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.Online.EventArguments; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer.CnCNet { public class LoadOrSaveGameOptionPresetWindow : XNAWindow { private bool _isLoad; private readonly XNALabel lblHeader; private readonly XNADropDownItem ddiCreatePresetItem; private readonly XNADropDownItem ddiSelectPresetItem; private readonly XNAClientButton btnLoadSave; private readonly XNAClientButton btnDelete; private readonly XNAClientDropDown ddPresetSelect; private readonly XNALabel lblNewPresetName; private readonly XNATextBox tbNewPresetName; public EventHandler PresetLoaded; public EventHandler PresetSaved; public LoadOrSaveGameOptionPresetWindow(WindowManager windowManager) : base(windowManager) { ClientRectangle = new Rectangle(0, 0, 325, 185); var margin = 10; lblHeader = new XNALabel(WindowManager); lblHeader.Name = nameof(lblHeader); lblHeader.FontIndex = 1; lblHeader.ClientRectangle = new Rectangle( margin, margin, 150, 22 ); var lblPresetName = new XNALabel(WindowManager); lblPresetName.Name = nameof(lblPresetName); lblPresetName.Text = "Preset Name".L10N("Client:Main:PresetName"); lblPresetName.ClientRectangle = new Rectangle( margin, lblHeader.Bottom + margin, 150, 18 ); ddiCreatePresetItem = new XNADropDownItem(); ddiCreatePresetItem.Text = "[Create New]".L10N("Client:Main:CreateNewPreset"); ddiSelectPresetItem = new XNADropDownItem(); ddiSelectPresetItem.Text = "[Select Preset]".L10N("Client:Main:SelectPreset"); ddiSelectPresetItem.Selectable = false; ddPresetSelect = new XNAClientDropDown(WindowManager); ddPresetSelect.Name = nameof(ddPresetSelect); ddPresetSelect.ClientRectangle = new Rectangle( 10, lblPresetName.Bottom + 2, 150, 22 ); ddPresetSelect.SelectedIndexChanged += DropDownPresetSelect_SelectedIndexChanged; lblNewPresetName = new XNALabel(WindowManager); lblNewPresetName.Name = nameof(lblNewPresetName); lblNewPresetName.Text = "New Preset Name".L10N("Client:Main:NewPresetName"); lblNewPresetName.ClientRectangle = new Rectangle( margin, ddPresetSelect.Bottom + margin, 150, 18 ); tbNewPresetName = new XNATextBox(WindowManager); tbNewPresetName.Name = nameof(tbNewPresetName); tbNewPresetName.ClientRectangle = new Rectangle( 10, lblNewPresetName.Bottom + 2, 150, 22 ); tbNewPresetName.TextChanged += (sender, args) => RefreshButtons(); btnLoadSave = new XNAClientButton(WindowManager); btnLoadSave.Name = nameof(btnLoadSave); btnLoadSave.LeftClick += BtnLoadSave_LeftClick; btnLoadSave.ClientRectangle = new Rectangle( margin, Height - UIDesignConstants.BUTTON_HEIGHT - margin, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT ); btnDelete = new XNAClientButton(WindowManager); btnDelete.Name = nameof(btnDelete); btnDelete.Text = "Delete".L10N("Client:Main:ButtonDelete"); btnDelete.LeftClick += BtnDelete_LeftClick; btnDelete.ClientRectangle = new Rectangle( btnLoadSave.Right + margin, btnLoadSave.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT ); var btnCancel = new XNAClientButton(WindowManager); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.ClientRectangle = new Rectangle( btnDelete.Right + margin, btnLoadSave.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT ); btnCancel.LeftClick += (sender, args) => Disable(); AddChild(lblHeader); AddChild(lblPresetName); AddChild(ddPresetSelect); AddChild(lblNewPresetName); AddChild(tbNewPresetName); AddChild(btnLoadSave); AddChild(btnDelete); AddChild(btnCancel); Disable(); } public override void Initialize() { Name = "LoadOrSaveGameOptionPresetWindow"; PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); base.Initialize(); } /// /// Show the window. /// /// The "mode" for the window: load vs save. public void Show(bool isLoad) { _isLoad = isLoad; lblHeader.Text = _isLoad ? "Load Preset".L10N("Client:Main:LoadPreset") : "Save Preset".L10N("Client:Main:SavePreset"); btnLoadSave.Text = _isLoad ? "Load".L10N("Client:Main:ButtonLoad") : "Save".L10N("Client:Main:ButtonSave"); if (_isLoad) ShowLoad(); else ShowSave(); RefreshButtons(); CenterOnParent(); Enable(); } /// /// Callback when the Preset drop down selection has changed /// private void DropDownPresetSelect_SelectedIndexChanged(object sender, EventArgs eventArgs) { if (!_isLoad) DropDownPresetSelect_SelectedIndexChanged_IsSave(); RefreshButtons(); } /// /// Callback when the Preset drop down selection has changed during "save" mode /// private void DropDownPresetSelect_SelectedIndexChanged_IsSave() { if (IsCreatePresetSelected) { // show the field to specify a new name when "create" option is selected in drop down tbNewPresetName.Enable(); lblNewPresetName.Enable(); } else { // hide the field to specify a new name when an existing preset is selected tbNewPresetName.Disable(); lblNewPresetName.Disable(); } } /// /// Refresh the state of the load/save button /// private void RefreshButtons() { if (_isLoad) btnLoadSave.Enabled = !IsSelectPresetSelected; else btnLoadSave.Enabled = !IsCreatePresetSelected || !IsNewPresetNameFieldEmpty; btnDelete.Enabled = !IsCreatePresetSelected && !IsSelectPresetSelected; } private bool IsCreatePresetSelected => ddPresetSelect.SelectedItem == ddiCreatePresetItem; private bool IsSelectPresetSelected => ddPresetSelect.SelectedItem == ddiSelectPresetItem; private bool IsNewPresetNameFieldEmpty => string.IsNullOrWhiteSpace(tbNewPresetName.Text); /// /// Populate the preset drop down from saved presets /// private void LoadPresets() { ddPresetSelect.Items.Clear(); ddPresetSelect.Items.Add(_isLoad ? ddiSelectPresetItem : ddiCreatePresetItem); ddPresetSelect.SelectedIndex = 0; ddPresetSelect.Items.AddRange(GameOptionPresets.Instance .GetPresetNames() .OrderBy(name => name) .Select(name => new XNADropDownItem() { Text = name })); } /// /// Show the current window in the "load" mode context /// private void ShowLoad() { LoadPresets(); // do not show fields to specify a preset name during "load" mode lblNewPresetName.Disable(); tbNewPresetName.Disable(); } /// /// Show the current window in the "save" mode context /// private void ShowSave() { LoadPresets(); // show fields to specify a preset name during "save" mode lblNewPresetName.Enable(); tbNewPresetName.Enable(); tbNewPresetName.Text = string.Empty; } private void BtnLoadSave_LeftClick(object sender, EventArgs e) { var selectedItem = ddPresetSelect.Items[ddPresetSelect.SelectedIndex]; if (_isLoad) { PresetLoaded?.Invoke(this, new GameOptionPresetEventArgs(selectedItem.Text)); } else { var presetName = IsCreatePresetSelected ? tbNewPresetName.Text : selectedItem.Text; PresetSaved?.Invoke(this, new GameOptionPresetEventArgs(presetName)); } Disable(); } private void BtnDelete_LeftClick(object sender, EventArgs e) { var selectedItem = ddPresetSelect.Items[ddPresetSelect.SelectedIndex]; var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Confirm Preset Delete".L10N("Client:Main:ConfirmPresetDeleteTitle"), "Are you sure you want to delete this preset?".L10N("Client:Main:ConfirmPresetDeleteText") + "\n\n" + selectedItem.Text); messageBox.YesClickedAction = box => { GameOptionPresets.Instance.DeletePreset(selectedItem.Text); ddPresetSelect.Items.Remove(selectedItem); ddPresetSelect.SelectedIndex = 0; }; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/MapSharingConfirmationPanel.cs ================================================ using ClientGUI; using ClientCore.Extensions; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A panel that is used to verify and display map sharing status. /// class MapSharingConfirmationPanel : XNAPanel { public MapSharingConfirmationPanel(WindowManager windowManager) : base(windowManager) { } private readonly string MapSharingRequestText = ("The game host has selected a map that\ndoesn't exist on your local installation.").L10N("Client:Main:MapSharingRequestText"); private readonly string MapSharingDownloadText = "Downloading map...".L10N("Client:Main:MapSharingDownloadText"); private readonly string MapSharingFailedText = ("Downloading map failed. The game host\nneeds to change the map or you will be\nunable to participate in the match.").L10N("Client:Main:MapSharingFailedText"); public event EventHandler MapDownloadConfirmed; private XNALabel lblDescription; private XNAClientButton btnDownload; public override void Initialize() { PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.TILED; Name = nameof(MapSharingConfirmationPanel); BackgroundTexture = AssetLoader.LoadTexture("msgboxform.png"); lblDescription = new XNALabel(WindowManager); lblDescription.Name = nameof(lblDescription); lblDescription.X = UIDesignConstants.EMPTY_SPACE_SIDES; lblDescription.Y = UIDesignConstants.EMPTY_SPACE_TOP; lblDescription.Text = MapSharingRequestText; AddChild(lblDescription); Width = lblDescription.Right + UIDesignConstants.EMPTY_SPACE_SIDES; btnDownload = new XNAClientButton(WindowManager); btnDownload.Name = nameof(btnDownload); btnDownload.Width = UIDesignConstants.BUTTON_WIDTH_92; btnDownload.Y = lblDescription.Bottom + UIDesignConstants.EMPTY_SPACE_TOP * 2; btnDownload.Text = "Download".L10N("Client:Main:ButtonDownload"); btnDownload.LeftClick += (s, e) => MapDownloadConfirmed?.Invoke(this, EventArgs.Empty); AddChild(btnDownload); btnDownload.CenterOnParentHorizontally(); Height = btnDownload.Bottom + UIDesignConstants.EMPTY_SPACE_BOTTOM; base.Initialize(); CenterOnParent(); Disable(); } public void ShowForMapDownload() { lblDescription.Text = MapSharingRequestText; btnDownload.AllowClick = true; Enable(); } public void SetDownloadingStatus() { lblDescription.Text = MapSharingDownloadText; btnDownload.AllowClick = false; } public void SetFailedStatus() { lblDescription.Text = MapSharingFailedText; btnDownload.AllowClick = false; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/PasswordRequestWindow.cs ================================================ using ClientGUI; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Multiplayer.CnCNet { internal class PasswordRequestWindow : XNAWindow { public PasswordRequestWindow(WindowManager windowManager, PrivateMessagingWindow privateMessagingWindow) : base(windowManager) { this.privateMessagingWindow = privateMessagingWindow; } public event EventHandler PasswordEntered; private XNATextBox tbPassword; private HostedCnCNetGame hostedGame; private PrivateMessagingWindow privateMessagingWindow; private bool pmWindowWasEnabled { get; set; } public override void Initialize() { Name = "PasswordRequestWindow"; BackgroundTexture = AssetLoader.LoadTexture("passwordquerybg.png"); var lblDescription = new XNALabel(WindowManager); lblDescription.Name = "lblDescription"; lblDescription.ClientRectangle = new Rectangle(12, 12, 0, 0); lblDescription.Text = "Please enter the password for the game and click OK.".L10N("Client:Main:EnterPasswordAndHitOK"); ClientRectangle = new Rectangle(0, 0, lblDescription.Width + 24, 110); tbPassword = new XNATextBox(WindowManager); tbPassword.Name = "tbPassword"; tbPassword.ClientRectangle = new Rectangle(lblDescription.X, lblDescription.Bottom + 12, Width - 24, 21); var btnOK = new XNAClientButton(WindowManager); btnOK.Name = "btnOK"; btnOK.ClientRectangle = new Rectangle(lblDescription.X, ClientRectangle.Bottom - 35, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnOK.Text = "OK".L10N("Client:Main:ButtonOK"); btnOK.LeftClick += BtnOK_LeftClick; var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = "btnCancel"; btnCancel.ClientRectangle = new Rectangle(Width - 104, btnOK.Y, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(lblDescription); AddChild(tbPassword); AddChild(btnOK); AddChild(btnCancel); base.Initialize(); CenterOnParent(); EnabledChanged += PasswordRequestWindow_EnabledChanged; tbPassword.EnterPressed += TextBoxPassword_EnterPressed; } private void TextBoxPassword_EnterPressed(object sender, EventArgs eventArgs) { BtnOK_LeftClick(this, eventArgs); } private void PasswordRequestWindow_EnabledChanged(object sender, EventArgs e) { if (Enabled) { WindowManager.SelectedControl = tbPassword; if (!privateMessagingWindow.Enabled) return; pmWindowWasEnabled = true; privateMessagingWindow.Disable(); } else if(pmWindowWasEnabled) { privateMessagingWindow.Enable(); } } private void BtnCancel_LeftClick(object sender, EventArgs e) { Disable(); } private void BtnOK_LeftClick(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbPassword.Text)) return; pmWindowWasEnabled = false; Disable(); PasswordEntered?.Invoke(this, new PasswordEventArgs(tbPassword.Text, hostedGame)); tbPassword.Text = string.Empty; } public void SetHostedGame(HostedCnCNetGame hostedGame) { this.hostedGame = hostedGame; } } public class PasswordEventArgs : EventArgs { public PasswordEventArgs(string password, HostedCnCNetGame hostedGame) { Password = password; HostedGame = hostedGame; } /// /// The password input by the user. /// public string Password { get; private set; } /// /// The game that the user is attempting to join. /// public HostedCnCNetGame HostedGame { get; private set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessageNotificationBox.cs ================================================ using ClientCore; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.IO; using System.Reflection; using DTAClient.Domain.Multiplayer.CnCNet; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A box that notifies users of new private messages, /// top-right of the game window. /// public class PrivateMessageNotificationBox : XNAPanel { const double DOWN_TIME_WAIT_SECONDS = 4.0; const double DOWN_MOVEMENT_RATE = 2.0; const double UP_MOVEMENT_RATE = 2.0; public PrivateMessageNotificationBox(WindowManager windowManager) : base(windowManager) { downTimeWaitTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS); } XNALabel lblSender; XNAPanel gameIconPanel; XNALabel lblMessage; TimeSpan downTime = TimeSpan.Zero; TimeSpan downTimeWaitTime; bool isDown = false; double locationY = -100.0; public override void Initialize() { Name = "PrivateMessageNotificationBox"; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 196), 1, 1); ClientRectangle = new Rectangle(WindowManager.RenderResolutionX - 300, -100, 300, 100); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; XNALabel lblHeader = new XNALabel(WindowManager); lblHeader.Name = "lblHeader"; lblHeader.FontIndex = 1; lblHeader.Text = "PRIVATE MESSAGE".L10N("Client:Main:PMHeader"); AddChild(lblHeader); lblHeader.CenterOnParent(); lblHeader.ClientRectangle = new Rectangle(lblHeader.X, 6, lblHeader.Width, lblHeader.Height); XNAPanel linePanel = new XNAPanel(WindowManager); linePanel.Name = "linePanel"; linePanel.ClientRectangle = new Rectangle(0, Height - 20, Width, 1); XNALabel lblHint = new XNALabel(WindowManager); lblHint.Name = "lblHint"; lblHint.RemapColor = UISettings.ActiveSettings.SubtleTextColor; lblHint.Text = "Press F4 to respond".L10N("Client:Main:F4ToRespond"); AddChild(lblHint); lblHint.CenterOnParent(); lblHint.ClientRectangle = new Rectangle(lblHint.X, linePanel.Y + 3, lblHint.Width, lblHint.Height); gameIconPanel = new XNAPanel(WindowManager); gameIconPanel.Name = "gameIconPanel"; gameIconPanel.ClientRectangle = new Rectangle(12, 30, 16, 16); gameIconPanel.DrawBorders = false; var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream dtaIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.dtaicon.png"); using var dtaIcon = Image.Load(dtaIconStream); gameIconPanel.BackgroundTexture = AssetLoader.TextureFromImage(dtaIcon); lblSender = new XNALabel(WindowManager); lblSender.Name = "lblSender"; lblSender.FontIndex = 1; lblSender.ClientRectangle = new Rectangle(gameIconPanel.Right + 3, gameIconPanel.Y, 0, 0); lblSender.Text = "Rampastring:"; lblMessage = new XNALabel(WindowManager); lblMessage.Name = "lblMessage"; lblMessage.ClientRectangle = new Rectangle(12, lblSender.Bottom + 6, 0, 0); lblMessage.RemapColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ReceivedPMColor); lblMessage.Text = "This is a test message."; AddChild(gameIconPanel); AddChild(linePanel); AddChild(lblSender); AddChild(lblMessage); base.Initialize(); } public void Show(Texture2D gameIcon, string sender, string message) { Visible = true; Enabled = true; gameIconPanel.BackgroundTexture = gameIcon; lblSender.Text = sender + ":"; lblMessage.Text = message; if (lblMessage.Right > Width) { while (lblMessage.Right > Width) { lblMessage.Text = lblMessage.Text.Remove(lblMessage.Text.Length - 1); } if (lblMessage.Text.Length > 3) { lblMessage.Text = lblMessage.Text.Remove(lblMessage.Text.Length - 3) + "..."; } } downTime = TimeSpan.Zero; isDown = true; } public void Hide() { isDown = false; locationY = -Height; ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); Visible = false; Enabled = false; } public override void Update(GameTime gameTime) { if (isDown) { if (locationY < 0) { locationY += DOWN_MOVEMENT_RATE; ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); } if (WindowManager.HasFocus) { downTime += gameTime.ElapsedGameTime; isDown = downTime < downTimeWaitTime; } } else { if (locationY > -Height) { locationY -= UP_MOVEMENT_RATE; ClientRectangle = new Rectangle(X, (int)locationY, Width, Height); } else { Visible = false; Enabled = false; } } base.Update(gameTime); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingPanel.cs ================================================ using ClientGUI; using Rampastring.XNAUI; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A panel that hides itself if it's clicked while none of its children /// are the focus of input. /// public class PrivateMessagingPanel : DarkeningPanel { public PrivateMessagingPanel(WindowManager windowManager) : base(windowManager) { } public override void OnLeftClick(InputEventArgs inputEventArgs) { inputEventArgs.Handled = true; if (GetActiveChild() == null) Hide(); base.OnLeftClick(inputEventArgs); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs ================================================ using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using ClientGUI; using DTAClient.Online; using DTAClient.Online.EventArguments; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using ClientCore.Enums; using ClientCore.Extensions; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer.CnCNet { public class PrivateMessagingWindow : XNAWindow, ISwitchable { private const int MESSAGES_INDEX = 0; private const int FRIEND_LIST_VIEW_INDEX = 1; private const int ALL_PLAYERS_VIEW_INDEX = 2; private const int RECENT_PLAYERS_VIEW_INDEX = 3; private const int LB_USERS_WIDTH = 150; private readonly string DEFAULT_PLAYERS_TEXT = "PLAYERS:".L10N("Client:Main:Players"); private readonly string RECENT_PLAYERS_TEXT = "RECENT PLAYERS:".L10N("Client:Main:RecentPlayers"); private CnCNetUserData cncnetUserData; private readonly PrivateMessageHandler privateMessageHandler; public PrivateMessagingWindow( WindowManager windowManager, CnCNetManager connectionManager, GameCollection gameCollection, CnCNetUserData cncnetUserData, PrivateMessageHandler privateMessageHandler ) : base(windowManager) { this.gameCollection = gameCollection; this.connectionManager = connectionManager; this.cncnetUserData = cncnetUserData; this.privateMessageHandler = privateMessageHandler; } private XNALabel lblPrivateMessaging; private XNAClientTabControl tabControl; private XNALabel lblPlayers; private XNAListBox lbUserList; private RecentPlayerTable mclbRecentPlayerList; private XNALabel lblMessages; private ChatListBox lbMessages; private XNATextBox tbMessageInput; private GlobalContextMenu globalContextMenu; private CnCNetManager connectionManager; private GameCollection gameCollection; private Texture2D unknownGameIcon; private Texture2D adminGameIcon; private Color personalMessageColor; private Color otherUserMessageColor; private string lastReceivedPMSender; private string lastConversationPartner; /// /// Holds the users that the local user has had conversations with /// during this client session. /// private List privateMessageUsers = new List(); private PrivateMessageNotificationBox notificationBox; private EnhancedSoundEffect sndPrivateMessageSound; private EnhancedSoundEffect sndMessageSound; /// /// Because the user cannot view PMs during a game, we store the latest /// PM received during a game in this variable and display it when the /// user has returned from the game. /// private PrivateMessage pmReceivedDuringGame; // These are used by the "invite to game" feature in the // context menu and are kept up-to-date by the lobby private string inviteChannelName; private string inviteGameName; private string inviteChannelPassword; private Action JoinUserAction; public override void Initialize() { Name = nameof(PrivateMessagingWindow); ClientRectangle = new Rectangle(0, 0, 600, 600); BackgroundTexture = AssetLoader.LoadTextureUncached("privatemessagebg.png"); var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream unknownIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.unknownicon.png"); using Stream cncnetIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.cncneticon.png"); unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream)); personalMessageColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.SentPMColor); otherUserMessageColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.ReceivedPMColor); lblPrivateMessaging = new XNALabel(WindowManager); lblPrivateMessaging.Name = nameof(lblPrivateMessaging); lblPrivateMessaging.FontIndex = 1; lblPrivateMessaging.Text = "PRIVATE MESSAGING".L10N("Client:Main:PMLabel"); AddChild(lblPrivateMessaging); lblPrivateMessaging.CenterOnParent(); lblPrivateMessaging.ClientRectangle = new Rectangle( lblPrivateMessaging.X, 12, lblPrivateMessaging.Width, lblPrivateMessaging.Height); tabControl = new XNAClientTabControl(WindowManager); tabControl.Name = nameof(tabControl); tabControl.ClientRectangle = new Rectangle(34, 50, 0, 0); tabControl.ClickSound = new EnhancedSoundEffect("button.wav"); tabControl.FontIndex = 1; tabControl.AddTab("Messages".L10N("Client:Main:MessagesTab"), UIDesignConstants.BUTTON_WIDTH_133); tabControl.AddTab("Friend List".L10N("Client:Main:FriendListTab"), UIDesignConstants.BUTTON_WIDTH_133); tabControl.AddTab("All Players".L10N("Client:Main:AllPlayersTab"), UIDesignConstants.BUTTON_WIDTH_133); tabControl.AddTab("Recent Players".L10N("Client:Main:RecentPlayersTab"), UIDesignConstants.BUTTON_WIDTH_133); tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged; lblPlayers = new XNALabel(WindowManager); lblPlayers.Name = nameof(lblPlayers); lblPlayers.ClientRectangle = new Rectangle(12, tabControl.Bottom + 24, 0, 0); lblPlayers.FontIndex = 1; lblPlayers.Text = DEFAULT_PLAYERS_TEXT; lbUserList = new XNAListBox(WindowManager); lbUserList.Name = nameof(lbUserList); lbUserList.ClientRectangle = new Rectangle(lblPlayers.X, lblPlayers.Bottom + 6, LB_USERS_WIDTH, Height - lblPlayers.Bottom - 18); lbUserList.RightClick += LbUserList_RightClick; lbUserList.SelectedIndexChanged += LbUserList_SelectedIndexChanged; lbUserList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbUserList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbUserList.DoubleLeftClick += UserList_LeftDoubleClick; lblMessages = new XNALabel(WindowManager); lblMessages.Name = nameof(lblMessages); lblMessages.ClientRectangle = new Rectangle(lbUserList.Right + 12, lblPlayers.Y, 0, 0); lblMessages.FontIndex = 1; lblMessages.Text = "MESSAGES:".L10N("Client:Main:Messages"); lbMessages = new ChatListBox(WindowManager); lbMessages.Name = nameof(lbMessages); lbMessages.ClientRectangle = new Rectangle(lblMessages.X, lbUserList.Y, Width - lblMessages.X - 12, lbUserList.Height - 25); lbMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbMessages.RightClick += ChatListBox_RightClick; tbMessageInput = new XNATextBox(WindowManager); tbMessageInput.Name = nameof(tbMessageInput); tbMessageInput.ClientRectangle = new Rectangle(lbMessages.X, lbMessages.Bottom + 6, lbMessages.Width, 19); tbMessageInput.EnterPressed += TbMessageInput_EnterPressed; tbMessageInput.MaximumTextLength = 200; tbMessageInput.Enabled = false; mclbRecentPlayerList = new RecentPlayerTable(WindowManager, connectionManager); mclbRecentPlayerList.ClientRectangle = new Rectangle(lbUserList.X, lbUserList.Y, lbMessages.Right - lbUserList.X, lbUserList.Height); mclbRecentPlayerList.PlayerRightClick += RecentPlayersList_RightClick; mclbRecentPlayerList.Disable(); globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, this); globalContextMenu.JoinEvent += PlayerContextMenu_JoinUser; notificationBox = new PrivateMessageNotificationBox(WindowManager); notificationBox.Enabled = false; notificationBox.Visible = false; notificationBox.LeftClick += NotificationBox_LeftClick; AddChild(tabControl); AddChild(lblPlayers); AddChild(lbUserList); AddChild(lblMessages); AddChild(lbMessages); AddChild(tbMessageInput); AddChild(mclbRecentPlayerList); AddChild(globalContextMenu); WindowManager.AddAndInitializeControl(notificationBox); base.Initialize(); CenterOnParent(); tabControl.SelectedTab = MESSAGES_INDEX; privateMessageHandler.PrivateMessageReceived += PrivateMessageHandler_PrivateMessageReceived; connectionManager.UserAdded += ConnectionManager_UserAdded; connectionManager.UserRemoved += ConnectionManager_UserRemoved; connectionManager.UserGameIndexUpdated += ConnectionManager_UserGameIndexUpdated; sndMessageSound = new EnhancedSoundEffect("message.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundMessageCooldown); sndPrivateMessageSound = new EnhancedSoundEffect("pm.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundPrivateMessageCooldown); sndMessageSound.Enabled = UserINISettings.Instance.MessageSound; GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; } private void ChatListBox_RightClick(object sender, EventArgs e) { if (lbMessages.HoveredIndex < 0 || lbMessages.HoveredIndex >= lbMessages.Items.Count) return; lbMessages.SelectedIndex = lbMessages.HoveredIndex; var chatMessage = lbMessages.SelectedItem.Tag as ChatMessage; if (chatMessage == null) return; globalContextMenu.Show(chatMessage, GetCursorPoint()); } private void UserList_LeftDoubleClick(object sender, EventArgs e) { if (lbUserList.SelectedItem != null) tabControl.SelectedTab = MESSAGES_INDEX; } private void RecentPlayersList_RightClick(object sender, RecentPlayerTableRightClickEventArgs e) => globalContextMenu.Show(e.IrcUser, GetCursorPoint()); private void ConnectionManager_UserGameIndexUpdated(object sender, UserEventArgs e) { var userItem = FindItemForName(e.User.Name); if (userItem != null) userItem.Texture = GetUserTexture(e.User); } private void ConnectionManager_UserRemoved(object sender, UserNameIndexEventArgs e) { var pmUser = privateMessageUsers.Find(pmsgUser => pmsgUser.IrcUser.Name == e.UserName); ChatMessage leaveMessage = null; if (pmUser != null) { leaveMessage = new ChatMessage(Color.White, string.Format("{0} is now offline.".L10N("Client:Main:PlayerOffline"), e.UserName)); pmUser.Messages.Add(leaveMessage); } if (tabControl.SelectedTab == ALL_PLAYERS_VIEW_INDEX) { if (e.UserIndex >= lbUserList.Items.Count || e.UserIndex < 0) return; if (e.UserIndex == lbUserList.SelectedIndex) { lbUserList.SelectedIndex = -1; } else if (e.UserIndex < lbUserList.SelectedIndex) { lbUserList.SelectedIndexChanged -= LbUserList_SelectedIndexChanged; lbUserList.SelectedIndex--; lbUserList.SelectedIndexChanged += LbUserList_SelectedIndexChanged; } lbUserList.Items.RemoveAt(e.UserIndex); } else { XNAListBoxItem lbItem = FindItemForName(e.UserName); if (lbItem != null) { lbItem.TextColor = UISettings.ActiveSettings.DisabledItemColor; lbItem.Texture = null; lbItem.Tag = null; if (lbItem == lbUserList.SelectedItem && leaveMessage != null) { tbMessageInput.Enabled = false; lbMessages.AddMessage(leaveMessage); } } } } private void ConnectionManager_UserAdded(object sender, UserEventArgs e) { var pmUser = privateMessageUsers.Find(pmsgUser => pmsgUser.IrcUser.Name == e.User.Name); ChatMessage joinMessage = null; if (pmUser != null) { joinMessage = new ChatMessage(string.Format("{0} is now online.".L10N("Client:Main:PlayerOnline"), e.User.Name)); pmUser.Messages.Add(joinMessage); } if (tabControl.SelectedTab == ALL_PLAYERS_VIEW_INDEX) { RefreshAllUsers(); } else // if (tabControl.SelectedTab == 0 or 1) { XNAListBoxItem lbItem = FindItemForName(e.User.Name); if (lbItem != null) { lbItem.Tag = e.User; lbItem.Texture = GetUserTexture(e.User); if (lbItem == lbUserList.SelectedItem) { tbMessageInput.Enabled = true; if (joinMessage != null) lbMessages.AddMessage(joinMessage); } } } } private void RefreshAllUsers() { lbUserList.SelectedIndexChanged -= LbUserList_SelectedIndexChanged; string selectedUserName = string.Empty; var selectedItem = lbUserList.SelectedItem; if (selectedItem != null) selectedUserName = selectedItem.Text; lbUserList.Clear(); foreach (var ircUser in connectionManager.UserList) { var item = new XNAListBoxItem(ircUser.Name); item.Tag = ircUser; item.Texture = GetUserTexture(ircUser); lbUserList.AddItem(item); } lbUserList.SelectedIndex = FindItemIndexForName(selectedUserName); if (lbUserList.SelectedIndex == -1) { // If we previously had an user selected and they now went offline, // clear the messages and message input tbMessageInput.Text = string.Empty; tbMessageInput.Enabled = false; lbMessages.Clear(); lbMessages.SelectedIndex = -1; lbMessages.TopIndex = 0; } lbUserList.SelectedIndexChanged += LbUserList_SelectedIndexChanged; } public void SetInviteChannelInfo(string channelName, string gameName, string channelPassword) { inviteChannelName = channelName; inviteGameName = gameName; inviteChannelPassword = channelPassword; } public void ClearInviteChannelInfo() => SetInviteChannelInfo(string.Empty, string.Empty, string.Empty); private void NotificationBox_LeftClick(object sender, EventArgs e) => SwitchOn(); private void LbUserList_RightClick(object sender, EventArgs e) { lbUserList.SelectedIndex = lbUserList.HoveredIndex; var ircUser = (IRCUser)lbUserList.SelectedItem?.Tag; if (ircUser == null) return; globalContextMenu.Show(new GlobalContextMenuData() { IrcUser = ircUser, inviteChannelName = inviteChannelName, inviteChannelPassword = inviteChannelPassword, inviteGameName = inviteGameName }, GetCursorPoint()); } private void PlayerContextMenu_JoinUser(object sender, JoinUserEventArgs args) { if (tabControl.SelectedTab == RECENT_PLAYERS_VIEW_INDEX) JoinUserAction(args.IrcUser, new RecentPlayerMessageView(WindowManager)); else JoinUserAction(args.IrcUser, lbMessages); } private void SharedUILogic_GameProcessExited() => WindowManager.AddCallback(new Action(HandleGameProcessExited), null); private void HandleGameProcessExited() { if (pmReceivedDuringGame != null) { ShowNotification(pmReceivedDuringGame.User, pmReceivedDuringGame.Message); pmReceivedDuringGame = null; } } private bool IsPlayerOnline(string playerName) => !string.IsNullOrEmpty(playerName) && connectionManager.UserList.Find(u => u.Name == playerName) != null; private void PrivateMessageHandler_PrivateMessageReceived(object sender, PrivateMessageEventArgs e) { if (UserINISettings.Instance.AllowPrivateMessagesFromState == (int)AllowPrivateMessagesFromEnum.None) return; PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == e.Sender); if (pmUser == null) { pmUser = new PrivateMessageUser(e.ircUser); privateMessageUsers.Add(pmUser); if (tabControl.SelectedTab == MESSAGES_INDEX) { string selecterUserName = string.Empty; if (lbUserList.SelectedItem != null) selecterUserName = lbUserList.SelectedItem.Text; lbUserList.Clear(); privateMessageUsers.ForEach(pmsgUser => AddPlayerToList(pmsgUser.IrcUser, IsPlayerOnline(pmsgUser.IrcUser.Name))); lbUserList.SelectedIndex = FindItemIndexForName(selecterUserName); } } bool isFriend = cncnetUserData.IsFriend(pmUser.IrcUser.Name); if (UserINISettings.Instance.AllowPrivateMessagesFromState == (int)AllowPrivateMessagesFromEnum.Friends && !isFriend) return; // Exclude messages from users not in the current channel if (!isFriend && UserINISettings.Instance.AllowPrivateMessagesFromState != (int)AllowPrivateMessagesFromEnum.All && connectionManager.MainChannel.Users.Find(e.Sender) == null) return; ChatMessage message = new ChatMessage(e.Sender, otherUserMessageColor, DateTime.Now, e.Message); pmUser.Messages.Add(message); lastReceivedPMSender = e.Sender; lastConversationPartner = e.Sender; if (!Visible) { HandleNotification(pmUser.IrcUser, e.Message); if (lbUserList.SelectedItem == null || lbUserList.SelectedItem.Text != e.Sender) return; } else if (lbUserList.SelectedItem == null || lbUserList.SelectedItem.Text != e.Sender) { HandleNotification(pmUser.IrcUser, e.Message); return; } lbMessages.AddMessage(message); if (sndMessageSound != null) sndMessageSound.Play(); } /// /// Displays a PM message if the user is not in-game, and queues /// it to be displayed after the game if the user is in-game. /// /// The sender of the private message. /// The contents of the private message. private void HandleNotification(IRCUser ircUser, string message) { if (!ProgramConstants.IsInGame) { ShowNotification(ircUser, message); } else pmReceivedDuringGame = new PrivateMessage(ircUser, message); } private void ShowNotification(IRCUser ircUser, string message) { if (!UserINISettings.Instance.DisablePrivateMessagePopups) notificationBox.Show(GetUserTexture(ircUser), ircUser.Name, message); else privateMessageHandler.IncrementUnreadMessageCount(); if (sndPrivateMessageSound != null) sndPrivateMessageSound.Play(); } private Predicate MatchItemForName(string userName) => item => ((IRCUser)item.Tag)?.Name == userName; private XNAListBoxItem FindItemForName(string userName) => lbUserList.Items.Find(MatchItemForName(userName)); private int FindItemIndexForName(string userName) => lbUserList.Items.FindIndex(MatchItemForName(userName)); private void TbMessageInput_EnterPressed(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbMessageInput.Text)) return; if (lbUserList.SelectedItem == null) return; string userName = lbUserList.SelectedItem.Text; connectionManager.SendCustomMessage(new QueuedMessage("PRIVMSG " + userName + " :" + tbMessageInput.Text, QueuedMessageType.CHAT_MESSAGE, 0)); PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); if (pmUser == null) { IRCUser iu = connectionManager.UserList.Find(u => u.Name == userName); if (iu == null) { Logger.Log("Null IRCUser in private messaging?"); return; } pmUser = new PrivateMessageUser(iu); privateMessageUsers.Add(pmUser); } ChatMessage sentMessage = new ChatMessage(ProgramConstants.PLAYERNAME, personalMessageColor, DateTime.Now, tbMessageInput.Text); pmUser.Messages.Add(sentMessage); lbMessages.AddMessage(sentMessage); if (sndMessageSound != null) sndMessageSound.Play(); lastConversationPartner = userName; if (tabControl.SelectedTab != MESSAGES_INDEX) { tabControl.SelectedTab = MESSAGES_INDEX; lbUserList.SelectedIndex = FindItemIndexForName(userName); } tbMessageInput.Text = string.Empty; } private void LbUserList_SelectedIndexChanged(object sender, EventArgs e) { lbMessages.Clear(); lbMessages.SelectedIndex = -1; lbMessages.TopIndex = 0; tbMessageInput.Text = string.Empty; if (lbUserList.SelectedItem == null) { tbMessageInput.Enabled = false; return; } var ircUser = (IRCUser)lbUserList.SelectedItem.Tag; tbMessageInput.Enabled = IsPlayerOnline(ircUser?.Name); var pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == lbUserList.SelectedItem.Text); if (pmUser == null) { return; } foreach (ChatMessage message in pmUser.Messages) { lbMessages.AddMessage(message); } lbMessages.ScrollToBottom(); } private void MessagesTabSelected() { ShowRecentPlayers(false); var _privateMessageUsers = privateMessageUsers.Select(pMsgUser => new { ircUser = pMsgUser.IrcUser, isFriend = cncnetUserData.FriendList.Contains(pMsgUser.IrcUser.Name), isOnline = connectionManager.UserList.Any(u => u.Name == pMsgUser.IrcUser.Name) }); var sortedPrivateMessageUsers = _privateMessageUsers .OrderBy(pMsgUser => !pMsgUser.isOnline) .ThenBy(pMsgUser => !pMsgUser.isFriend) .ThenBy(pMsguser => pMsguser.ircUser.Name); foreach (var pMsgUser in sortedPrivateMessageUsers) AddPlayerToList(pMsgUser.ircUser, pMsgUser.isOnline); } private void FriendsListTabSelected() { ShowRecentPlayers(false); var friends = cncnetUserData.FriendList.Select(friendName => { var ircUser = connectionManager.UserList.Find(u => u.Name == friendName); return new { ircUser = ircUser ?? new IRCUser(friendName), isOnline = ircUser != null }; }); friends .OrderBy(friend => !friend.isOnline) .ThenBy(friend => friend.ircUser.Name) .ToList() .ForEach(friend => AddPlayerToList(friend.ircUser, friend.isOnline)); } private void RecentPlayersTabSelected() { ShowRecentPlayers(true); var recentPlayers = cncnetUserData.RecentList.OrderByDescending(rp => rp.GameTime); mclbRecentPlayerList.ClearItems(); foreach (RecentPlayer recentPlayer in recentPlayers) mclbRecentPlayerList.AddRecentPlayer(recentPlayer); } private void AllPlayersTabSelected() { ShowRecentPlayers(false); foreach (var user in connectionManager.UserList) AddPlayerToList(user, true); } private void ShowRecentPlayers(bool show) { if (show) { lbMessages.Disable(); tbMessageInput.Disable(); lblMessages.Disable(); lbUserList.Disable(); lblPlayers.Text = RECENT_PLAYERS_TEXT; mclbRecentPlayerList.Enable(); } else { lbMessages.Enable(); tbMessageInput.Enable(); lblMessages.Enable(); lbUserList.Enable(); lblPlayers.Text = DEFAULT_PLAYERS_TEXT; mclbRecentPlayerList.Disable(); } } private void TabControl_SelectedIndexChanged(object sender, EventArgs e) { lbMessages.Clear(); lbMessages.SelectedIndex = -1; lbMessages.TopIndex = 0; lbUserList.Clear(); lbUserList.SelectedIndex = -1; lbUserList.TopIndex = 0; tbMessageInput.Text = string.Empty; switch (tabControl.SelectedTab) { case MESSAGES_INDEX: MessagesTabSelected(); break; case FRIEND_LIST_VIEW_INDEX: FriendsListTabSelected(); break; case ALL_PLAYERS_VIEW_INDEX: AllPlayersTabSelected(); break; case RECENT_PLAYERS_VIEW_INDEX: RecentPlayersTabSelected(); break; } } private void AddPlayerToList(IRCUser user, bool isOnline, string label = null) { XNAListBoxItem lbItem = new XNAListBoxItem(); lbItem.Text = label ?? user.Name; lbItem.TextColor = isOnline ? UISettings.ActiveSettings.AltColor : UISettings.ActiveSettings.DisabledItemColor; lbItem.Tag = user; lbItem.Texture = isOnline ? GetUserTexture(user) : null; lbUserList.AddItem(lbItem); } private Texture2D GetUserTexture(IRCUser user) { if (user.GameID < 0 || user.GameID >= gameCollection.GameList.Count) return unknownGameIcon; else return gameCollection.GameList[user.GameID].Texture; } /// /// Prepares a recipient for sending a private message. /// /// public void InitPM(string name) { Visible = true; Enabled = true; // Check if we've already talked with the user during this session // and if so, open the old conversation int pmUserIndex = privateMessageUsers.FindIndex( pmUser => pmUser.IrcUser.Name == name); if (pmUserIndex > -1) { tabControl.SelectedTab = MESSAGES_INDEX; lbUserList.SelectedIndex = FindItemIndexForName(name); WindowManager.SelectedControl = tbMessageInput; return; } if (cncnetUserData.IsFriend(name)) { // If we haven't talked with the user, check if they are a friend and if so, // let's enter the friend list and talk to them there tabControl.SelectedTab = FRIEND_LIST_VIEW_INDEX; } else { // If the user isn't a friend, switch to the "all players" view and // open the conversation there tabControl.SelectedTab = ALL_PLAYERS_VIEW_INDEX; } lbUserList.SelectedIndex = FindItemIndexForName(name); if (lbUserList.SelectedIndex > -1) { WindowManager.SelectedControl = tbMessageInput; lbUserList.TopIndex = lbUserList.SelectedIndex > -1 ? lbUserList.SelectedIndex : 0; } if (lbUserList.LastIndex - lbUserList.TopIndex < lbUserList.NumberOfLinesOnList - 1) lbUserList.ScrollToBottom(); } public void SwitchOn() { tabControl.SelectedTab = MESSAGES_INDEX; notificationBox.Hide(); WindowManager.SelectedControl = null; privateMessageHandler.ResetUnreadMessageCount(); if (Visible) { if (!string.IsNullOrEmpty(lastReceivedPMSender)) { int index = FindItemIndexForName(lastReceivedPMSender); if (index > -1) lbUserList.SelectedIndex = index; } } else { Enable(); if (!string.IsNullOrEmpty(lastConversationPartner)) { int index = FindItemIndexForName(lastConversationPartner); if (index > -1) { lbUserList.SelectedIndex = index; WindowManager.SelectedControl = tbMessageInput; } } } } public void SetJoinUserAction(Action joinUserAction) { JoinUserAction = joinUserAction; } public void SwitchOff() => Disable(); public string GetSwitchName() => "Private Messaging".L10N("Client:Main:PrivateMessaging"); /// /// A class for storing a private message in memory. /// class PrivateMessage { public PrivateMessage(IRCUser user, string message) { User = user; Message = message; } public IRCUser User; public string Message; } class RecentPlayerMessageView : IMessageView { private readonly WindowManager windowManager; public RecentPlayerMessageView(WindowManager windowManager) { this.windowManager = windowManager; } public void AddMessage(ChatMessage message) => XNAMessageBox.Show(windowManager, "Message".L10N("Client:Main:MessageTitle"), message.Message); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTable.cs ================================================ using System; using System.Collections.Generic; using DTAClient.Online; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer.CnCNet { public class RecentPlayerTable : XNAMultiColumnListBox { private readonly CnCNetManager connectionManager; public EventHandler PlayerRightClick; public RecentPlayerTable(WindowManager windowManager, CnCNetManager connectionManager) : base(windowManager) { this.connectionManager = connectionManager; } public override void Initialize() { AllowRightClickUnselect = false; base.Initialize(); AddColumn("Player".L10N("Client:Main:RecentPlayerPlayer")); AddColumn("Game".L10N("Client:Main:RecentPlayerGame")); AddColumn("Date/Time".L10N("Client:Main:RecentPlayerDateTime")); } public void AddRecentPlayer(RecentPlayer recentPlayer) { IRCUser iu = connectionManager.UserList.Find(u => u.Name == recentPlayer.PlayerName); bool isOnline = true; if (iu == null) { iu = new IRCUser(recentPlayer.PlayerName); isOnline = false; } var textColor = isOnline ? UISettings.ActiveSettings.AltColor : UISettings.ActiveSettings.DisabledItemColor; AddItem(new List() { new XNAListBoxItem(recentPlayer.PlayerName, textColor) { Tag = iu }, new XNAListBoxItem(recentPlayer.GameName, textColor), new XNAListBoxItem(recentPlayer.GameTime.ToLocalTime().ToString("ddd, MMM d, yyyy @ h:mm tt"), textColor) }); } private XNAPanel CreateColumnHeader(string headerText) { XNALabel xnaLabel = new XNALabel(WindowManager); xnaLabel.FontIndex = HeaderFontIndex; xnaLabel.X = 3; xnaLabel.Y = 2; xnaLabel.Text = headerText; XNAPanel header = new XNAPanel(WindowManager); header.Height = xnaLabel.Height + 3; var width = Width / 3; if (DrawListBoxBorders) header.Width = width + 1; else header.Width = width; header.AddChild(xnaLabel); return header; } private void AddColumn(string headerText) { var header = CreateColumnHeader(headerText); var xnaListBox = new XNAListBox(WindowManager); xnaListBox.RightClick += ListBox_RightClick; AddColumn(header, xnaListBox); } private void ListBox_RightClick(object sender, EventArgs e) { if (HoveredIndex < 0 || HoveredIndex >= ItemCount) return; SelectedIndex = HoveredIndex; var selectedItem = GetItem(0, SelectedIndex); PlayerRightClick?.Invoke(this, new RecentPlayerTableRightClickEventArgs((IRCUser)selectedItem.Tag)); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTableRightClickEventArgs.cs ================================================ using System; using DTAClient.Online; namespace DTAClient.DXGUI.Multiplayer.CnCNet { public class RecentPlayerTableRightClickEventArgs : EventArgs { public IRCUser IrcUser { get; set; } public RecentPlayerTableRightClickEventArgs(IRCUser ircUser) { IrcUser = ircUser; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs ================================================ using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using System.Reflection; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A list box for listing CnCNet tunnel servers. /// class TunnelListBox : XNAMultiColumnListBox { private static readonly Dictionary CountryCodeFlagOffsets = ParseCountryCodeFlagOffsets(); private const int FLAG_WIDTH = 16; private const int FLAG_HEIGHT = 16; public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) : base(windowManager) { this.tunnelHandler = tunnelHandler; tunnelHandler.TunnelsRefreshed += TunnelHandler_TunnelsRefreshed; tunnelHandler.TunnelPinged += TunnelHandler_TunnelPinged; SelectedIndexChanged += TunnelListBox_SelectedIndexChanged; int headerHeight = (int)Renderer.GetTextDimensions("Name", HeaderFontIndex).Y; Width = 466; Height = LineHeight * 12 + headerHeight + 3; PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); using Stream flagsStream = Assembly.GetAssembly(typeof(GameCollection)).GetManifestResourceStream("DTAClient.Icons.flags16.png"); var flagsPNG = SixLabors.ImageSharp.Image.Load(flagsStream); flagsSpriteSheet = AssetLoader.TextureFromImage(flagsPNG); var flagListBox = new FlagListBox(windowManager, tunnelHandler, flagsSpriteSheet); flagListBox.FontIndex = FontIndex; flagListBox.LineHeight = LineHeight; var flagHeader = new XNAPanel(windowManager); flagHeader.Width = 20; flagHeader.Height = headerHeight + 3; AddColumn(flagHeader, flagListBox); AddColumn("Name".L10N("Client:Main:NameHeader"), 210); AddColumn("Official".L10N("Client:Main:OfficialHeader"), 70); AddColumn("Ping".L10N("Client:Main:PingHeader"), 76); AddColumn("Players".L10N("Client:Main:PlayersHeader"), 90); AllowRightClickUnselect = false; AllowKeyboardInput = true; } public event EventHandler ListRefreshed; private readonly TunnelHandler tunnelHandler; private Texture2D flagsSpriteSheet; private int bestTunnelIndex = 0; private int lowestTunnelRating = int.MaxValue; private bool isManuallySelectedTunnel; private string manuallySelectedTunnelAddress; /// /// Selects a tunnel from the list with the given address. /// /// The address of the tunnel server to select. public void SelectTunnel(string address) { int index = tunnelHandler.Tunnels.FindIndex(t => t.Address == address); if (index > -1) { SelectedIndex = index; isManuallySelectedTunnel = true; manuallySelectedTunnelAddress = address; } } /// /// Gets whether or not a tunnel from the list with the given address is selected. /// /// The address of the tunnel server /// True if tunnel with given address is selected, otherwise false. public bool IsTunnelSelected(string address) => tunnelHandler.Tunnels.FindIndex(t => t.Address == address) == SelectedIndex; private void TunnelHandler_TunnelsRefreshed(object sender, EventArgs e) { ClearItems(); int tunnelIndex = 0; foreach (CnCNetTunnel tunnel in tunnelHandler.Tunnels) { List info = new List(); info.Add(""); // Flag column info.Add(tunnel.Name); info.Add(Conversions.BooleanToString(tunnel.Official, BooleanStringStyle.YESNO)); if (tunnel.PingInMs < 0) info.Add("Unknown".L10N("Client:Main:UnknownPing")); else info.Add(tunnel.PingInMs + " ms"); info.Add(tunnel.Clients + " / " + tunnel.MaxClients); AddItem(info, true); XNAListBoxItem flagItem = GetItem(0, tunnelIndex); if (flagItem != null) flagItem.Tag = GetFlagRectangle(tunnel.CountryCode); if ((tunnel.Official || tunnel.Recommended) && tunnel.PingInMs > -1) { int rating = GetTunnelRating(tunnel); if (rating < lowestTunnelRating) { bestTunnelIndex = tunnelIndex; lowestTunnelRating = rating; } } tunnelIndex++; } if (tunnelHandler.Tunnels.Count > 0) { if (!isManuallySelectedTunnel) { SelectedIndex = bestTunnelIndex; isManuallySelectedTunnel = false; } else { int manuallySelectedIndex = tunnelHandler.Tunnels.FindIndex(t => t.Address == manuallySelectedTunnelAddress); if (manuallySelectedIndex == -1) { SelectedIndex = bestTunnelIndex; isManuallySelectedTunnel = false; } else SelectedIndex = manuallySelectedIndex; } } ListRefreshed?.Invoke(this, EventArgs.Empty); } private void TunnelHandler_TunnelPinged(int tunnelIndex) { XNAListBoxItem lbItem = GetItem(3, tunnelIndex); CnCNetTunnel tunnel = tunnelHandler.Tunnels[tunnelIndex]; if (tunnel.PingInMs == -1) lbItem.Text = "Unknown".L10N("Client:Main:UnknownPing"); else { lbItem.Text = tunnel.PingInMs + " ms"; int rating = GetTunnelRating(tunnel); if (isManuallySelectedTunnel) return; if ((tunnel.Recommended || tunnel.Official) && rating < lowestTunnelRating) { bestTunnelIndex = tunnelIndex; lowestTunnelRating = rating; SelectedIndex = tunnelIndex; } } } private int GetTunnelRating(CnCNetTunnel tunnel) { double usageRatio = (double)tunnel.Clients / tunnel.MaxClients; if (usageRatio == 0) usageRatio = 0.1; usageRatio *= 100.0; return Convert.ToInt32(Math.Pow(tunnel.PingInMs, 2.0) * usageRatio); } private void TunnelListBox_SelectedIndexChanged(object sender, EventArgs e) { if (!IsValidIndexSelected()) return; isManuallySelectedTunnel = true; manuallySelectedTunnelAddress = tunnelHandler.Tunnels[SelectedIndex].Address; } private static Dictionary ParseCountryCodeFlagOffsets() { // 16px version from // https://github.com/lafeber/world-flags-sprite // Offsets are located in the css files. return new Dictionary { ["ad"] = 352, ["ae"] = 368, ["af"] = 384, ["ag"] = 400, ["ai"] = 416, ["al"] = 432, ["am"] = 448, ["ao"] = 464, ["aq"] = 480, ["ar"] = 496, ["as"] = 512, ["at"] = 528, ["au"] = 544, ["aw"] = 560, ["ax"] = 576, ["az"] = 592, ["ba"] = 608, ["bb"] = 624, ["bd"] = 640, ["be"] = 656, ["bf"] = 672, ["bg"] = 688, ["bh"] = 704, ["bi"] = 720, ["bj"] = 736, ["bl"] = 1424, ["bm"] = 752, ["bn"] = 768, ["bo"] = 784, ["bq"] = 2752, ["br"] = 800, ["bs"] = 816, ["bt"] = 832, ["bv"] = 2768, ["bw"] = 848, ["by"] = 864, ["bz"] = 880, ["ca"] = 896, ["cd"] = 912, ["cf"] = 928, ["cg"] = 944, ["ch"] = 960, ["ci"] = 976, ["ck"] = 992, ["cl"] = 1008, ["cm"] = 1024, ["cn"] = 1040, ["co"] = 1056, ["cp"] = 1424, ["cr"] = 1072, ["cu"] = 1088, ["cv"] = 1104, ["cw"] = 3920, ["cy"] = 1120, ["cz"] = 1136, ["de"] = 1152, ["dj"] = 1168, ["dk"] = 1184, ["dm"] = 1200, ["do"] = 1216, ["dz"] = 1232, ["ec"] = 1248, ["ee"] = 1264, ["eg"] = 1280, ["eh"] = 1296, ["er"] = 1312, ["es"] = 1328, ["et"] = 1344, ["fi"] = 1360, ["fj"] = 1376, ["fm"] = 1392, ["fo"] = 1408, ["fr"] = 1424, ["ga"] = 1440, ["gb"] = 1456, ["gd"] = 1472, ["ge"] = 1488, ["gg"] = 1504, ["gh"] = 1520, ["gi"] = 1536, ["gl"] = 1552, ["gm"] = 1568, ["gn"] = 1584, ["gp"] = 1600, ["gq"] = 1616, ["gr"] = 1632, ["gt"] = 1648, ["gu"] = 1664, ["gw"] = 1680, ["gy"] = 1696, ["hk"] = 1712, ["hn"] = 1728, ["hr"] = 1744, ["ht"] = 1760, ["hu"] = 1776, ["id"] = 1792, ["ie"] = 1808, ["il"] = 1824, ["im"] = 1840, ["in"] = 1856, ["iq"] = 1872, ["ir"] = 1888, ["is"] = 1904, ["it"] = 1920, ["je"] = 1936, ["jm"] = 1952, ["jo"] = 1968, ["jp"] = 1984, ["ke"] = 2000, ["kg"] = 2016, ["kh"] = 2032, ["ki"] = 2048, ["km"] = 2064, ["kn"] = 2080, ["kp"] = 2096, ["kr"] = 2112, ["kw"] = 2128, ["ky"] = 2144, ["kz"] = 2160, ["la"] = 2176, ["lb"] = 2192, ["lc"] = 2208, ["li"] = 2224, ["lk"] = 2240, ["lr"] = 2256, ["ls"] = 2272, ["lt"] = 2288, ["lu"] = 2304, ["lv"] = 2320, ["ly"] = 2336, ["ma"] = 2352, ["mc"] = 1792, ["md"] = 2368, ["me"] = 2384, ["mf"] = 1424, ["mg"] = 2400, ["mh"] = 2416, ["mk"] = 2432, ["ml"] = 2448, ["mm"] = 2464, ["mn"] = 2480, ["mo"] = 2496, ["mq"] = 2512, ["mr"] = 2528, ["ms"] = 2544, ["mt"] = 2560, ["mu"] = 2576, ["mv"] = 2592, ["mw"] = 2608, ["mx"] = 2624, ["my"] = 2640, ["mz"] = 2656, ["na"] = 2672, ["nc"] = 2688, ["ne"] = 2704, ["ng"] = 2720, ["ni"] = 2736, ["nl"] = 2752, ["no"] = 2768, ["np"] = 2784, ["nq"] = 2768, ["nr"] = 2800, ["nu"] = 3952, ["nz"] = 2816, ["om"] = 2832, ["pa"] = 2848, ["pe"] = 2864, ["pf"] = 2880, ["pg"] = 2896, ["ph"] = 2912, ["pk"] = 2928, ["pl"] = 2944, ["pr"] = 2960, ["ps"] = 2976, ["pt"] = 2992, ["pw"] = 3008, ["py"] = 3024, ["qa"] = 3040, ["re"] = 3056, ["ro"] = 3072, ["rs"] = 3088, ["ru"] = 3104, ["rw"] = 3120, ["sa"] = 3136, ["sb"] = 3152, ["sc"] = 3168, ["sd"] = 3184, ["se"] = 3200, ["sg"] = 3216, ["sh"] = 1456, ["si"] = 3232, ["sj"] = 2768, ["sk"] = 3248, ["sl"] = 3264, ["sm"] = 3280, ["sn"] = 3296, ["so"] = 3312, ["sr"] = 3328, ["ss"] = 3936, ["st"] = 3344, ["sv"] = 3360, ["sx"] = 3904, ["sy"] = 3376, ["sz"] = 3392, ["tc"] = 3408, ["td"] = 3424, ["tg"] = 3440, ["th"] = 3456, ["tj"] = 3472, ["tl"] = 3488, ["tm"] = 3504, ["tn"] = 3520, ["to"] = 3536, ["tr"] = 3552, ["tt"] = 3568, ["tv"] = 3584, ["tw"] = 3600, ["tz"] = 3616, ["ua"] = 3632, ["ug"] = 3648, ["us"] = 3664, ["uy"] = 3680, ["uz"] = 3696, ["va"] = 3712, ["vc"] = 3728, ["ve"] = 3744, ["vg"] = 3760, ["vi"] = 3776, ["vn"] = 3792, ["vu"] = 3808, ["ws"] = 3824, ["ye"] = 3840, ["yt"] = 1424, ["za"] = 3856, ["zm"] = 3872, ["zw"] = 3888 }; } private static Rectangle? GetFlagRectangle(string countryCode) { if (string.IsNullOrEmpty(countryCode)) return null; string code = countryCode.ToLowerInvariant(); if (CountryCodeFlagOffsets.TryGetValue(code, out int yOffset)) { return new Rectangle(0, yOffset, FLAG_WIDTH, FLAG_HEIGHT); } return null; } /// /// Custom listbox that draws country flags. /// private class FlagListBox : XNAListBox { private readonly TunnelHandler tunnelHandler; private readonly Texture2D flagsSpriteSheet; public FlagListBox(WindowManager windowManager, TunnelHandler tunnelHandler, Texture2D flagsSpriteSheet) : base(windowManager) { this.tunnelHandler = tunnelHandler; this.flagsSpriteSheet = flagsSpriteSheet; } public override void Draw(GameTime gameTime) { DrawPanel(); int height = 2 - (ViewTop % LineHeight); for (int i = TopIndex; i < Items.Count; i++) { if (height > Height) break; Rectangle? flagRect = Items[i].Tag as Rectangle?; if (flagRect.HasValue) { int x = (Width - FLAG_WIDTH) / 2; DrawTexture(flagsSpriteSheet, flagRect.Value, new Rectangle(x, height, FLAG_WIDTH, FLAG_HEIGHT), Color.White); } height += LineHeight; } if (DrawBorders) DrawPanelBorders(); DrawChildren(gameTime); } } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs ================================================ using ClientGUI; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Multiplayer.CnCNet { /// /// A window for selecting a CnCNet tunnel server. /// class TunnelSelectionWindow : XNAWindow { public TunnelSelectionWindow(WindowManager windowManager, TunnelHandler tunnelHandler) : base(windowManager) { this.tunnelHandler = tunnelHandler; } public event EventHandler TunnelSelected; private readonly TunnelHandler tunnelHandler; private TunnelListBox lbTunnelList; private XNALabel lblDescription; private XNAClientButton btnApply; private string originalTunnelAddress; public override void Initialize() { if (Initialized) return; Name = "TunnelSelectionWindow"; BackgroundTexture = AssetLoader.LoadTexture("gamecreationoptionsbg.png"); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lblDescription = new XNALabel(WindowManager); lblDescription.Name = nameof(lblDescription); lblDescription.Text = "Line 1" + Environment.NewLine + "Line 2"; lblDescription.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; lblDescription.Y = UIDesignConstants.EMPTY_SPACE_TOP + UIDesignConstants.CONTROL_VERTICAL_MARGIN; AddChild(lblDescription); lbTunnelList = new TunnelListBox(WindowManager, tunnelHandler); lbTunnelList.Name = nameof(lbTunnelList); lbTunnelList.Y = lblDescription.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN; lbTunnelList.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; AddChild(lbTunnelList); lbTunnelList.SelectedIndexChanged += LbTunnelList_SelectedIndexChanged; btnApply = new XNAClientButton(WindowManager); btnApply.Name = nameof(btnApply); btnApply.Width = UIDesignConstants.BUTTON_WIDTH_92; btnApply.Height = UIDesignConstants.BUTTON_HEIGHT; btnApply.Text = "Apply".L10N("Client:Main:ButtonApply"); btnApply.X = UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; btnApply.Y = lbTunnelList.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN * 3; AddChild(btnApply); btnApply.LeftClick += BtnApply_LeftClick; var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.Width = UIDesignConstants.BUTTON_WIDTH_92; btnCancel.Height = UIDesignConstants.BUTTON_HEIGHT; btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.Y = btnApply.Y; AddChild(btnCancel); btnCancel.LeftClick += BtnCancel_LeftClick; Width = lbTunnelList.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN + UIDesignConstants.EMPTY_SPACE_SIDES; Height = btnApply.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM; btnCancel.X = Width - btnCancel.Width - UIDesignConstants.EMPTY_SPACE_SIDES - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; base.Initialize(); } private void BtnApply_LeftClick(object sender, EventArgs e) { Disable(); if (!lbTunnelList.IsValidIndexSelected()) return; CnCNetTunnel tunnel = tunnelHandler.Tunnels[lbTunnelList.SelectedIndex]; TunnelSelected?.Invoke(this, new TunnelEventArgs(tunnel)); } private void BtnCancel_LeftClick(object sender, EventArgs e) => Disable(); private void LbTunnelList_SelectedIndexChanged(object sender, EventArgs e) => btnApply.AllowClick = !lbTunnelList.IsTunnelSelected(originalTunnelAddress) && lbTunnelList.IsValidIndexSelected(); /// /// Sets the window's description and selects the tunnel server /// with the given address. /// /// The window description. /// The address of the tunnel server to select. public void Open(string description, string tunnelAddress = null) { lblDescription.Text = description; originalTunnelAddress = tunnelAddress; if (!string.IsNullOrWhiteSpace(tunnelAddress)) lbTunnelList.SelectTunnel(tunnelAddress); else lbTunnelList.SelectedIndex = -1; if (lbTunnelList.SelectedIndex > -1) { lbTunnelList.SetTopIndex(0); while (lbTunnelList.SelectedIndex > lbTunnelList.LastIndex) lbTunnelList.TopIndex++; } btnApply.AllowClick = false; Enable(); } } class TunnelEventArgs : EventArgs { public TunnelEventArgs(CnCNetTunnel tunnel) { Tunnel = tunnel; } public CnCNetTunnel Tunnel { get; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameFiltersPanel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using ClientCore; using ClientGUI; using ClientCore.Extensions; using DTAClient.DXGUI.Multiplayer.GameLobby; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { /// /// Custom scroll panel that exposes ContentPanel for adding children /// internal class GameFiltersScrollPanel : XNAScrollPanel { public GameFiltersScrollPanel(WindowManager windowManager) : base(windowManager) { } public XNAPanel GetContentPanel() => ContentPanel; } public class GameFiltersPanel : XNAPanel { private const int minPlayerCount = 2; private const int maxPlayerCount = 8; private const int GAP = 12; private const int BOTTOM_PANEL_HEIGHT = 60; private GameFiltersScrollPanel scrollPanel; private XNAPanel bottomPanel; private XNAClientCheckBox chkBoxFriendsOnly; private XNAClientCheckBox chkBoxHideLockedGames; private XNAClientCheckBox chkBoxHidePasswordedGames; private XNAClientCheckBox chkBoxHideIncompatibleGames; private XNAClientDropDown ddMaxPlayerCount; private GameLobbyBase gameLobby; private List gameOptionFilterControls = []; private bool gameOptionFiltersCreated = false; private class GameOptionFilterControl { public string OptionName { get; set; } public bool IsCheckbox { get; set; } public XNAClientDropDown DropDown { get; set; } public XNALabel Label { get; set; } public XNAPanel IconPanel { get; set; } public string EnabledIcon { get; set; } public string DisabledIcon { get; set; } } public GameFiltersPanel(WindowManager windowManager, GameLobbyBase gameLobby) : base(windowManager) { this.gameLobby = gameLobby; } public override void Initialize() { base.Initialize(); Name = "GameFiltersWindow"; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0), Width, Height); // Create scroll panel for filters content scrollPanel = new GameFiltersScrollPanel(WindowManager); scrollPanel.Name = "FiltersScrollPanel"; scrollPanel.ClientRectangle = new Rectangle(0, 0, Width, Height - BOTTOM_PANEL_HEIGHT); scrollPanel.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 200), 1, 1); scrollPanel.DrawBorders = false; // Create bottom panel for Save/Cancel buttons bottomPanel = new XNAPanel(WindowManager); bottomPanel.Name = "BottomButtonPanel"; bottomPanel.ClientRectangle = new Rectangle(0, Height - BOTTOM_PANEL_HEIGHT, Width, BOTTOM_PANEL_HEIGHT); bottomPanel.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); bottomPanel.DrawBorders = false; var lblTitle = new XNALabel(WindowManager); lblTitle.Name = nameof(lblTitle); lblTitle.Text = "Game Filters".L10N("Client:Main:GameFilters"); lblTitle.ClientRectangle = new Rectangle( GAP, GAP, 120, UIDesignConstants.BUTTON_HEIGHT ); chkBoxFriendsOnly = new XNAClientCheckBox(WindowManager); chkBoxFriendsOnly.Name = nameof(chkBoxFriendsOnly); chkBoxFriendsOnly.Text = "Show Friend Games Only".L10N("Client:Main:FriendGameOnly"); chkBoxFriendsOnly.ClientRectangle = new Rectangle( GAP, lblTitle.Y + UIDesignConstants.BUTTON_HEIGHT + GAP, 0, 0 ); chkBoxHideLockedGames = new XNAClientCheckBox(WindowManager); chkBoxHideLockedGames.Name = nameof(chkBoxHideLockedGames); chkBoxHideLockedGames.Text = "Hide Locked Games".L10N("Client:Main:HideLockedGame"); chkBoxHideLockedGames.ClientRectangle = new Rectangle( GAP, chkBoxFriendsOnly.Y + UIDesignConstants.BUTTON_HEIGHT + GAP, 0, 0 ); chkBoxHidePasswordedGames = new XNAClientCheckBox(WindowManager); chkBoxHidePasswordedGames.Name = nameof(chkBoxHidePasswordedGames); chkBoxHidePasswordedGames.Text = "Hide Passworded Games".L10N("Client:Main:HidePasswordGame"); chkBoxHidePasswordedGames.ClientRectangle = new Rectangle( GAP, chkBoxHideLockedGames.Y + UIDesignConstants.BUTTON_HEIGHT + GAP, 0, 0 ); chkBoxHideIncompatibleGames = new XNAClientCheckBox(WindowManager); chkBoxHideIncompatibleGames.Name = nameof(chkBoxHideIncompatibleGames); chkBoxHideIncompatibleGames.Text = "Hide Incompatible Games".L10N("Client:Main:HideIncompatibleGame"); chkBoxHideIncompatibleGames.ClientRectangle = new Rectangle( GAP, chkBoxHidePasswordedGames.Y + UIDesignConstants.BUTTON_HEIGHT + GAP, 0, 0 ); ddMaxPlayerCount = new XNAClientDropDown(WindowManager); ddMaxPlayerCount.Name = nameof(ddMaxPlayerCount); ddMaxPlayerCount.ClientRectangle = new Rectangle( GAP, chkBoxHideIncompatibleGames.Y + UIDesignConstants.BUTTON_HEIGHT + GAP, 40, UIDesignConstants.BUTTON_HEIGHT ); for (int i = minPlayerCount; i <= maxPlayerCount; i++) { ddMaxPlayerCount.AddItem(i.ToString()); } var lblMaxPlayerCount = new XNALabel(WindowManager); lblMaxPlayerCount.Name = nameof(lblMaxPlayerCount); lblMaxPlayerCount.Text = "Max Player Count".L10N("Client:Main:MaxPlayerCount"); lblMaxPlayerCount.ClientRectangle = new Rectangle( ddMaxPlayerCount.X + ddMaxPlayerCount.Width + GAP, ddMaxPlayerCount.Y, 0, UIDesignConstants.BUTTON_HEIGHT ); var btnResetDefaults = new XNAClientButton(WindowManager); btnResetDefaults.Name = nameof(btnResetDefaults); btnResetDefaults.Text = "Reset Defaults".L10N("Client:Main:ResetDefaults"); btnResetDefaults.ClientRectangle = new Rectangle( GAP, ddMaxPlayerCount.Y + UIDesignConstants.BUTTON_HEIGHT + GAP, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT ); btnResetDefaults.LeftClick += BtnResetDefaults_LeftClick; var btnSave = new XNAClientButton(WindowManager); btnSave.Name = nameof(btnSave); btnSave.Text = "Save".L10N("Client:Main:ButtonSave"); btnSave.ClientRectangle = new Rectangle( GAP, (BOTTOM_PANEL_HEIGHT - UIDesignConstants.BUTTON_HEIGHT) / 2, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT ); btnSave.LeftClick += BtnSave_LeftClick; var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.ClientRectangle = new Rectangle( Width - GAP - UIDesignConstants.BUTTON_WIDTH_92, (BOTTOM_PANEL_HEIGHT - UIDesignConstants.BUTTON_HEIGHT) / 2, UIDesignConstants.BUTTON_WIDTH_92, UIDesignConstants.BUTTON_HEIGHT ); btnCancel.LeftClick += BtnCancel_LeftClick; scrollPanel.GetContentPanel().AddChild(lblTitle); scrollPanel.GetContentPanel().AddChild(chkBoxFriendsOnly); scrollPanel.GetContentPanel().AddChild(chkBoxHideLockedGames); scrollPanel.GetContentPanel().AddChild(chkBoxHidePasswordedGames); scrollPanel.GetContentPanel().AddChild(chkBoxHideIncompatibleGames); scrollPanel.GetContentPanel().AddChild(lblMaxPlayerCount); scrollPanel.GetContentPanel().AddChild(ddMaxPlayerCount); scrollPanel.GetContentPanel().AddChild(btnResetDefaults); bottomPanel.AddChild(btnSave); bottomPanel.AddChild(btnCancel); AddChild(scrollPanel); AddChild(bottomPanel); } private void CreateGameOptionFilters() { // Note: broadcasted checkboxes are converted to dropdowns so we can have a third - undefined - value. if (gameLobby == null) return; var broadcastableCheckboxes = gameLobby.CheckBoxes.Where(cb => cb.BroadcastToLobby && cb.ShowInFilters).ToList(); var broadcastableDropdowns = gameLobby.DropDowns.Where(dd => dd.BroadcastToLobby && dd.ShowInFilters).ToList(); if (broadcastableCheckboxes.Count == 0 && broadcastableDropdowns.Count == 0) return; int currentY = ddMaxPlayerCount.Y + UIDesignConstants.BUTTON_HEIGHT + GAP; const int iconLabelSpacing = 6; const int itemVerticalSpacing = 4; const int minLabelRowHeight = 18; int dropdownWidth = (scrollPanel.Width - (GAP * 3)) / 2; var divider = CreateDivider(currentY, scrollPanel.Width); scrollPanel.GetContentPanel().AddChild(divider); currentY += divider.Height + GAP; int leftColumnX = GAP; int rightColumnX = scrollPanel.Width / 2 + GAP / 2; int filterIndex = 0; int maxItemHeight = 0; // Create filters for broadcastable checkboxes foreach (var checkbox in broadcastableCheckboxes) { var filterControl = new GameOptionFilterControl { OptionName = checkbox.Name, IsCheckbox = true, EnabledIcon = checkbox.EnabledIcon, DisabledIcon = checkbox.DisabledIcon }; Texture2D icon = null; if (!string.IsNullOrEmpty(checkbox.EnabledIcon)) icon = AssetLoader.LoadTexture(checkbox.EnabledIcon); int iconWidth = icon?.Width ?? 0; int iconHeight = icon?.Height ?? 0; bool isLeftColumn = (filterIndex % 2 == 0); int columnX = isLeftColumn ? leftColumnX : rightColumnX; int rowY = currentY + (filterIndex / 2) * maxItemHeight; XNAPanel iconPanel = null; if (icon != null) { iconPanel = new XNAPanel(WindowManager) { Name = $"icon{checkbox.Name}Filter", ClientRectangle = new Rectangle(columnX, rowY, iconWidth, iconHeight), DrawBorders = false, BackgroundTexture = icon }; filterControl.IconPanel = iconPanel; } var label = new XNALabel(WindowManager) { Name = $"lbl{checkbox.Name}Filter", Text = checkbox.Text + ":", ClientRectangle = new Rectangle( columnX + iconWidth + (iconWidth > 0 ? iconLabelSpacing : 0), rowY, 0, UIDesignConstants.BUTTON_HEIGHT) }; int topRowHeight = Math.Max(iconHeight, minLabelRowHeight); var dropdown = new XNAClientDropDown(WindowManager) { Name = $"dd{checkbox.Name}Filter", ClientRectangle = new Rectangle(columnX, rowY + topRowHeight + itemVerticalSpacing, dropdownWidth, UIDesignConstants.BUTTON_HEIGHT) }; // "All" item has no icon dropdown.AddItem(new XNADropDownItem { Text = "All".L10N("Client:Main:FilterAllGames"), Texture = null }); Texture2D enabledIconTexture = null; if (!string.IsNullOrEmpty(checkbox.EnabledIcon)) enabledIconTexture = AssetLoader.LoadTexture(checkbox.EnabledIcon); dropdown.AddItem(new XNADropDownItem { Text = "On".L10N("Client:Main:FilterOn"), Texture = enabledIconTexture }); Texture2D disabledIconTexture = null; if (!string.IsNullOrEmpty(checkbox.DisabledIcon)) disabledIconTexture = AssetLoader.LoadTexture(checkbox.DisabledIcon); dropdown.AddItem(new XNADropDownItem { Text = "Off".L10N("Client:Main:FilterOff"), Texture = disabledIconTexture }); dropdown.SelectedIndex = 0; filterControl.DropDown = dropdown; filterControl.Label = label; int itemHeight = topRowHeight + itemVerticalSpacing + UIDesignConstants.BUTTON_HEIGHT + GAP; if (itemHeight > maxItemHeight) maxItemHeight = itemHeight; gameOptionFilterControls.Add(filterControl); if (iconPanel != null) scrollPanel.GetContentPanel().AddChild(iconPanel); scrollPanel.GetContentPanel().AddChild(dropdown); scrollPanel.GetContentPanel().AddChild(label); filterIndex++; } // Create filters for broadcastable dropdowns foreach (var lobbyDropdown in broadcastableDropdowns) { var filterControl = new GameOptionFilterControl { OptionName = lobbyDropdown.Name, IsCheckbox = false }; // For dropdowns with multiple icons, show the first one initially Texture2D icon = lobbyDropdown.Items.Count > 0 ? lobbyDropdown.Items[0].Texture : null; int iconWidth = icon?.Width ?? 0; int iconHeight = icon?.Height ?? 0; bool isLeftColumn = (filterIndex % 2 == 0); int columnX = isLeftColumn ? leftColumnX : rightColumnX; int rowY = currentY + (filterIndex / 2) * maxItemHeight; XNAPanel iconPanel = null; if (icon != null) { iconPanel = new XNAPanel(WindowManager) { Name = $"icon{lobbyDropdown.Name}Filter", ClientRectangle = new Rectangle(columnX, rowY, iconWidth, iconHeight), DrawBorders = false, BackgroundTexture = icon }; filterControl.IconPanel = iconPanel; } var label = new XNALabel(WindowManager) { Name = $"lbl{lobbyDropdown.Name}Filter", Text = lobbyDropdown.OptionName, ClientRectangle = new Rectangle( columnX + iconWidth + (iconWidth > 0 ? iconLabelSpacing : 0), rowY, 0, UIDesignConstants.BUTTON_HEIGHT) }; int topRowHeight = Math.Max(iconHeight, minLabelRowHeight); var dropdown = new XNAClientDropDown(WindowManager) { Name = $"dd{lobbyDropdown.Name}Filter", ClientRectangle = new Rectangle(columnX, rowY + topRowHeight + itemVerticalSpacing, dropdownWidth, UIDesignConstants.BUTTON_HEIGHT) }; dropdown.AddItem(new XNADropDownItem { Text = "All".L10N("Client:Main:FilterAllGames"), Texture = null }); for (int i = 0; i < lobbyDropdown.Items.Count; i++) { var item = lobbyDropdown.Items[i]; dropdown.AddItem(new XNADropDownItem { Text = item.Text, Tag = item.Tag, Texture = item.Texture }); } dropdown.SelectedIndex = 0; filterControl.DropDown = dropdown; filterControl.Label = label; int itemHeight = topRowHeight + itemVerticalSpacing + UIDesignConstants.BUTTON_HEIGHT + GAP; if (itemHeight > maxItemHeight) maxItemHeight = itemHeight; gameOptionFilterControls.Add(filterControl); if (iconPanel != null) scrollPanel.GetContentPanel().AddChild(iconPanel); scrollPanel.GetContentPanel().AddChild(dropdown); scrollPanel.GetContentPanel().AddChild(label); filterIndex++; } if (gameOptionFilterControls.Count > 0) { int numRows = (filterIndex + 1) / 2; currentY += numRows * maxItemHeight; var secondDivider = CreateDivider(currentY, scrollPanel.Width); scrollPanel.GetContentPanel().AddChild(secondDivider); currentY += secondDivider.Height + GAP; var btnResetDefaults = scrollPanel.GetContentPanel().Children.FirstOrDefault(c => c.Name == "btnResetDefaults") as XNAClientButton; if (btnResetDefaults != null) { btnResetDefaults.ClientRectangle = new Rectangle( GAP, currentY, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT ); } UpdateScrollContentHeight(); } } private void BtnSave_LeftClick(object sender, EventArgs e) { Save(); Disable(); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Cancel(); } private void BtnResetDefaults_LeftClick(object sender, EventArgs e) { ResetDefaults(); } private void Save() { var userIniSettings = UserINISettings.Instance; userIniSettings.ShowFriendGamesOnly.Value = chkBoxFriendsOnly.Checked; userIniSettings.HideLockedGames.Value = chkBoxHideLockedGames.Checked; userIniSettings.HidePasswordedGames.Value = chkBoxHidePasswordedGames.Checked; userIniSettings.HideIncompatibleGames.Value = chkBoxHideIncompatibleGames.Checked; userIniSettings.MaxPlayerCount.Value = int.Parse(ddMaxPlayerCount.SelectedItem.Text); // Save game option filters (only non-default values) var gameOptionFiltersSection = userIniSettings.SettingsIni.GetSection(UserINISettings.GAME_OPTION_FILTERS); gameOptionFiltersSection?.RemoveAllKeys(); foreach (var filterControl in gameOptionFilterControls) { if (filterControl.IsCheckbox) { // UI: 0 = All, 1 = On, 2 = Off // Storage: null = All, 1 = On, 0 = Off int? filterValue = filterControl.DropDown.SelectedIndex switch { 0 => null, // All 1 => 1, // On 2 => 0, // Off _ => null }; if (filterValue != null) // Only save if not "All" userIniSettings.SetGameOptionFilterValue(filterControl.OptionName, filterValue); } else { // UI: 0 = All, 1+ = game option indices // Storage: null = All, otherwise actual index int? filterValue = filterControl.DropDown.SelectedIndex == 0 ? null : filterControl.DropDown.SelectedIndex - 1; if (filterValue != null) // if not "All" userIniSettings.SetGameOptionFilterValue(filterControl.OptionName, filterValue); } } UserINISettings.Instance.SaveSettings(); } private void Load() { var userIniSettings = UserINISettings.Instance; chkBoxFriendsOnly.Checked = userIniSettings.ShowFriendGamesOnly.Value; chkBoxHideLockedGames.Checked = userIniSettings.HideLockedGames.Value; chkBoxHidePasswordedGames.Checked = userIniSettings.HidePasswordedGames.Value; chkBoxHideIncompatibleGames.Checked = userIniSettings.HideIncompatibleGames.Value; ddMaxPlayerCount.SelectedIndex = ddMaxPlayerCount.Items.FindIndex(i => i.Text == userIniSettings.MaxPlayerCount.Value.ToString()); foreach (var filterControl in gameOptionFilterControls) { int? filterValue = userIniSettings.GetGameOptionFilterValue(filterControl.OptionName); if (filterControl.IsCheckbox) { // Storage: null = All, 1 = On, 0 = Off // UI: 0 = All, 1 = On, 2 = Off filterControl.DropDown.SelectedIndex = filterValue switch { null => 0, // All 1 => 1, // On 0 => 2, // Off _ => 0 }; } else { // Storage: null = All, otherwise actual index // UI: 0 = All, 1+ = game option indices filterControl.DropDown.SelectedIndex = filterValue == null ? 0 : filterValue.Value + 1; } } } private void ResetDefaults() { UserINISettings.Instance.ResetGameFilters(); Load(); } public void Show() { if (!gameOptionFiltersCreated) { CreateGameOptionFilters(); gameOptionFiltersCreated = true; } Load(); Enable(); } public void Cancel() { Disable(); } private void UpdateScrollContentHeight() { var content = scrollPanel.GetContentPanel(); int bottom = content.Children.Max(c => c.Bottom); content.Height = bottom + GAP; } private XNAPanel CreateDivider(int y, int width, int height = 1) { var dividerPanel = new XNAPanel(WindowManager) { DrawBorders = true, ClientRectangle = new Rectangle(0, y, width, height) }; return dividerPanel; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameInformationIconOnlyPanel.cs ================================================ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { /// /// A panel for showing a game information icon without text. /// public class GameInformationIconOnlyPanel : XNAPanel { private readonly Texture2D icon; public GameInformationIconOnlyPanel(WindowManager windowManager, Texture2D icon) : base(windowManager) { this.icon = icon; DrawBorders = false; } public override void Draw(GameTime gameTime) { base.Draw(gameTime); if (icon == null) return; DrawTexture(icon, new Rectangle(0, 0, icon.Width, icon.Height), Color.White); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameInformationIconPanel.cs ================================================ using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { /// /// A panel for showing a game information icon next to its associated label. /// public class GameInformationIconPanel : XNAPanel { private readonly Texture2D icon; private readonly string label; private readonly int maxIconWidth; private const int iconLabelSpacing = 6; public int FontIndex = 0; public GameInformationIconPanel(WindowManager windowManager, Texture2D icon, string label, int maxIconWidth = 0) : base(windowManager) { this.icon = icon; this.label = label; this.maxIconWidth = maxIconWidth; DrawBorders = false; } public override void Draw(GameTime gameTime) { base.Draw(gameTime); if (icon == null) return; var textSize = Renderer.GetTextDimensions(label, FontIndex); int textHeight = (int)textSize.Y; int iconY = (textHeight - icon.Height) / 2; if (iconY < 0) iconY = 0; DrawTexture(icon, new Rectangle(0, iconY, icon.Width, icon.Height), Color.White); int panelHeight = Math.Max(icon.Height, textHeight); float textY = (panelHeight - textHeight) / 2f; int textStartX = maxIconWidth > 0 ? maxIconWidth + iconLabelSpacing : icon.Width + iconLabelSpacing; DrawString(label, FontIndex, new Vector2(textStartX, textY), UISettings.ActiveSettings.TextColor); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs ================================================ using System; using System.Diagnostics; using System.Collections.Generic; using System.Linq; using ClientCore; using ClientCore.Extensions; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using Image = SixLabors.ImageSharp.Image; namespace DTAClient.DXGUI.Multiplayer { /// /// A UI panel that displays information about a hosted CnCNet or LAN game. /// public class GameInformationPanel : XNAPanel { private const int MAX_PLAYERS = 8; public GameInformationPanel(WindowManager windowManager, MapLoader mapLoader, GameLobbyBase gameLobby = null) : base(windowManager) { this.mapLoader = mapLoader; this.gameLobby = gameLobby; DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; } private MapLoader mapLoader; private GameLobbyBase gameLobby; private XNALabel lblGameInformation; private XNALabel lblGameMode; private XNALabel lblMap; private XNALabel lblGameVersion; private XNALabel lblHost; private XNALabel lblPing; private XNALabel lblPlayers; private XNALabel lblSkillLevel; private XNALabel[] lblPlayerNames; private XNAPanel pnlIconLegend; private XNAPanel pnlGameOptions; private GenericHostedGame game = null; /// /// Indicates whether `mapPreviewTexture` needs to be disposed before loading the next texture. This is true if the current `mapPreviewTexture` was created from a map preview image and not assigned from the shared `noMapPreviewTexture`. /// private bool mapPreviewTextureNeedsDispose = false; private Texture2D mapPreviewTexture = null; private Texture2D noMapPreviewTexture = null; private Texture2D txLockedGame; private Texture2D txIncompatibleGame; private Texture2D txPasswordedGame; private const int leftColumnPositionX = 10; private int rightColumnPositionX = 0; private int mapPreviewPositionY = 0; private const int columnMargin = 10; private const int topStartingPositionY = 30; private const int rowHeight = 24; private const int initialPanelHeight = 260; private const int columnWidth = 235; private const int maxPreviewHeight = 150; private const int mapPreviewMargin = 15; private const int playerNameRowHeight = 20; private const int playerColumn2OffsetX = 115; private const int legendTopSpacing = 15; private const int legendIconHeight = 18; private const int legendPadding = 5; private const int legendIconPadding = 2; private const int gameInfoLabelTopPadding = 6; private const int mapPreviewHorizontalMargin = 10; private const int mapPreviewVerticalMargin = 20; private string[] skillLevelOptions; public override void Initialize() { ClientRectangle = new Rectangle(0, 0, columnWidth * 2, initialPanelHeight); BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lblGameInformation = new XNALabel(WindowManager); lblGameInformation.FontIndex = 1; lblGameInformation.Text = "GAME INFORMATION".L10N("Client:Main:GameInfo"); if (AssetLoader.AssetExists("noMapPreview.png")) noMapPreviewTexture = AssetLoader.LoadTexture("noMapPreview.png"); txLockedGame = AssetLoader.LoadTexture("lockedgame.png"); txIncompatibleGame = AssetLoader.LoadTexture("incompatible.png"); txPasswordedGame = AssetLoader.LoadTexture("passwordedgame.png"); rightColumnPositionX = Width / 2 - columnMargin; mapPreviewPositionY = topStartingPositionY + (rowHeight * 2 + mapPreviewMargin); // 2 Labels down, incase map name spills to next line // Right Column // Includes Game mode, Map name, and the Map preview (See RenderMapPreview for that) lblGameMode = new XNALabel(WindowManager); lblGameMode.ClientRectangle = new Rectangle(rightColumnPositionX, topStartingPositionY, 0, 0); lblMap = new XNALabel(WindowManager); lblMap.ClientRectangle = new Rectangle(rightColumnPositionX, topStartingPositionY + rowHeight, 0, 0); // Left Column // Includes Host, Ping, Version, and Players lblHost = new XNALabel(WindowManager); lblHost.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY, 0, 0); lblPing = new XNALabel(WindowManager); lblPing.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + rowHeight, 0, 0); lblGameVersion = new XNALabel(WindowManager); lblGameVersion.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + (rowHeight * 2), 0, 0); lblSkillLevel = new XNALabel(WindowManager); lblSkillLevel.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + (rowHeight * 3), 0, 0); lblPlayers = new XNALabel(WindowManager); lblPlayers.ClientRectangle = new Rectangle(leftColumnPositionX, topStartingPositionY + (rowHeight * 4), 0, 0); lblPlayerNames = new XNALabel[MAX_PLAYERS]; for (int i = 0; i < lblPlayerNames.Length / 2; i++) { XNALabel lblPlayerName1 = new XNALabel(WindowManager); lblPlayerName1.ClientRectangle = new Rectangle(lblPlayers.X, lblPlayers.Y + rowHeight + i * playerNameRowHeight, 0, 0); lblPlayerName1.RemapColor = UISettings.ActiveSettings.AltColor; XNALabel lblPlayerName2 = new XNALabel(WindowManager); lblPlayerName2.ClientRectangle = new Rectangle(lblPlayers.X + playerColumn2OffsetX, lblPlayerName1.Y, 0, 0); lblPlayerName2.RemapColor = UISettings.ActiveSettings.AltColor; AddChild(lblPlayerName1); AddChild(lblPlayerName2); lblPlayerNames[i] = lblPlayerName1; lblPlayerNames[(lblPlayerNames.Length / 2) + i] = lblPlayerName2; } pnlIconLegend = new XNAPanel(WindowManager); int legendY = lblPlayers.Y + rowHeight + (MAX_PLAYERS / 2 * playerNameRowHeight) + legendTopSpacing; pnlIconLegend.ClientRectangle = new Rectangle(0, legendY, columnWidth, 0); pnlIconLegend.DrawBorders = false; pnlGameOptions = new XNAPanel(WindowManager); pnlGameOptions.ClientRectangle = new Rectangle(0, legendY, columnWidth * 2, 0); pnlGameOptions.DrawBorders = false; AddChild(lblGameMode); AddChild(lblMap); AddChild(lblGameVersion); AddChild(lblHost); AddChild(lblPing); AddChild(lblPlayers); AddChild(lblGameInformation); AddChild(lblSkillLevel); AddChild(pnlGameOptions); AddChild(pnlIconLegend); lblGameInformation.CenterOnParent(); lblGameInformation.ClientRectangle = new Rectangle(lblGameInformation.X, gameInfoLabelTopPadding, lblGameInformation.Width, lblGameInformation.Height); skillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(','); base.Initialize(); } public void SetInfo(GenericHostedGame game) { ClearInfo(); this.game = game; string translatedMapName = "Unknown".L10N("Client:Main:Unknown"); if (!string.IsNullOrEmpty(game.MapHash) && mapLoader != null) { Map map = mapLoader.FindMapByHash(game.MapHash); if (map != null) translatedMapName = map.Name ?? map.UntranslatedName; else if (!string.IsNullOrEmpty(game.Map)) translatedMapName = game.Map; // fallback to broadcasted name } else if (!string.IsNullOrEmpty(game.Map)) { translatedMapName = game.Map; } string translatedGameModeName = string.IsNullOrEmpty(game.GameMode) ? "Unknown".L10N("Client:Main:Unknown") : game.GameMode.L10N($"INI:GameModes:{game.GameMode}:UIName", notify: false); lblGameMode.Text = Renderer.GetStringWithLimitedWidth("Game mode:".L10N("Client:Main:GameInfoGameMode") + " " + Renderer.GetSafeString(translatedGameModeName, lblGameMode.FontIndex), lblGameMode.FontIndex, Width - lblGameMode.X); lblGameMode.Visible = true; lblMap.Text = Renderer.GetStringWithLimitedWidth("Map:".L10N("Client:Main:GameInfoMap") + " " + Renderer.GetSafeString(translatedMapName, lblMap.FontIndex), lblMap.FontIndex, Width - lblMap.X); lblMap.Visible = true; lblMap.Text = Renderer.FixText(lblMap.Text, lblMap.FontIndex, columnWidth).Text; lblMap.Visible = true; lblGameVersion.Text = "Game version:".L10N("Client:Main:GameInfoGameVersion") + " " + Renderer.GetSafeString(game.GameVersion, lblGameVersion.FontIndex); lblGameVersion.Visible = true; lblHost.Text = "Host:".L10N("Client:Main:GameInfoHost") + " " + Renderer.GetSafeString(game.HostName, lblHost.FontIndex); lblHost.Visible = true; lblPing.Text = game.Ping > 0 ? "Ping:".L10N("Client:Main:GameInfoPing") + " " + game.Ping.ToString() + " ms" : "Ping: Unknown".L10N("Client:Main:GameInfoPingUnknown"); lblPing.Visible = true; lblPlayers.Visible = true; lblPlayers.Text = "Players".L10N("Client:Main:GameInfoPlayers") + " (" + game.Players.Length + " / " + game.MaxPlayers + "):"; for (int i = 0; i < game.Players.Length && i < MAX_PLAYERS; i++) { lblPlayerNames[i].Visible = true; lblPlayerNames[i].Text = Renderer.GetSafeString(game.Players[i], lblPlayerNames[i].FontIndex); } for (int i = game.Players.Length; i < MAX_PLAYERS; i++) { lblPlayerNames[i].Visible = false; } string skillLevel = skillLevelOptions[game.SkillLevel]; string localizedSkillLevel = skillLevel.L10N($"INI:ClientDefinitions:SkillLevel:{game.SkillLevel}"); lblSkillLevel.Text = "Preferred Skill Level:".L10N("Client:Main:GameInfoSkillLevel") + " " + localizedSkillLevel; lblSkillLevel.Visible = true; lblGameInformation.Visible = true; if (mapLoader != null && !string.IsNullOrEmpty(game.MapHash)) { Debug.Assert(!mapPreviewTextureNeedsDispose, "previous texture must be disposed before loading a new texture"); Map map = mapLoader.FindMapByHash(game.MapHash); Image mapPreviewImage = map != null ? mapLoader.GetCachedPreviewImageFromMap(map, syncLoadOnCacheMiss: false) : null; if (mapPreviewImage != null) { mapPreviewTexture = AssetLoader.TextureFromImage(mapPreviewImage); mapPreviewTextureNeedsDispose = true; } else if (noMapPreviewTexture != null) { Debug.Assert(!noMapPreviewTexture.IsDisposed, "noMapPreviewTexture should never be disposed."); mapPreviewTexture = noMapPreviewTexture; mapPreviewTextureNeedsDispose = false; } else { mapPreviewTexture = null; mapPreviewTextureNeedsDispose = false; } } else { if (mapPreviewTextureNeedsDispose && mapPreviewTexture != null && !mapPreviewTexture.IsDisposed) { mapPreviewTexture.Dispose(); } mapPreviewTexture = null; mapPreviewTextureNeedsDispose = false; } SetGameOptionsInfo(game); SetLegendInfo(game); } private void SetGameOptionsInfo(GenericHostedGame game) { foreach (XNAControl xnaControl in pnlGameOptions.Children.ToList()) pnlGameOptions.RemoveChild(xnaControl); if (gameLobby == null || !(game is HostedCnCNetGame cncnetGame) || cncnetGame.BroadcastedGameOptionValues == null) { pnlGameOptions.Visible = false; return; } var broadcastableSettings = gameLobby.GetBroadcastableSettings(); var optionIconsWithText = new List<(Texture2D icon, string text, int sortOrder)>(); var optionIconsOnly = new List<(Texture2D icon, int sortOrder)>(); for (int i = 0; i < broadcastableSettings.Count && i < cncnetGame.BroadcastedGameOptionValues.Length; i++) { var setting = broadcastableSettings[i]; int value = cncnetGame.BroadcastedGameOptionValues[i]; if (setting is GameLobbyCheckBox checkbox && checkbox.ShowInGameInformationPanel) { bool isChecked = value != 0; string iconName = isChecked ? checkbox.EnabledIcon : checkbox.DisabledIcon; if (!string.IsNullOrEmpty(iconName)) { Texture2D icon = AssetLoader.LoadTexture(iconName); if (icon != null) { if (checkbox.ShowInGameInformationPanelAsIconOnly) { // Show icon only optionIconsOnly.Add((icon, checkbox.SortOrder)); } else { // Show with text string text = $"{checkbox.Text}: {(isChecked ? "On".L10N("Client:Main:On") : "Off".L10N("Client:Main:Off"))}"; optionIconsWithText.Add((icon, text, checkbox.SortOrder)); } } } } else if (setting is GameLobbyDropDown dropdown && dropdown.ShowInGameInformationPanel) { if (value >= 0 && value < dropdown.Items.Count) { Texture2D icon = dropdown.Items[value].Texture; if (icon != null) { if (dropdown.ShowInGameInformationPanelAsIconOnly) { // Show icon only optionIconsOnly.Add((icon, dropdown.SortOrder)); } else { // Show with text string text = $"{dropdown.OptionName}: {dropdown.Items[value].Text}"; optionIconsWithText.Add((icon, text, dropdown.SortOrder)); } } } } } if (optionIconsWithText.Count == 0 && optionIconsOnly.Count == 0) { pnlGameOptions.Visible = false; return; } int gameOptionsY = lblPlayers.Y + rowHeight + (MAX_PLAYERS / 2 * playerNameRowHeight) + legendTopSpacing; pnlGameOptions.ClientRectangle = new Rectangle(0, gameOptionsY, columnWidth * 2, 0); var divider = CreateDivider(0); pnlGameOptions.AddChild(divider); int currentY = divider.Bottom + legendPadding; // First show icons in a row if (optionIconsOnly.Count > 0) { var sortedIconsOnly = optionIconsOnly.OrderBy(x => x.sortOrder).ToList(); int iconX = leftColumnPositionX; const int iconSpacing = 4; int maxIconHeight = sortedIconsOnly.Max(x => x.icon.Height); foreach (var (icon, _) in sortedIconsOnly) { var iconPanel = new GameInformationIconOnlyPanel(WindowManager, icon); iconPanel.ClientRectangle = new Rectangle(iconX, currentY, icon.Width, icon.Height); pnlGameOptions.AddChild(iconPanel); iconX += icon.Width + iconSpacing; } currentY += maxIconHeight + legendPadding; } // Then show icons with text (two columns) if (optionIconsWithText.Count > 0) { var sortedIconsWithText = optionIconsWithText.OrderBy(x => x.sortOrder).ToList(); int maxIconWidth = sortedIconsWithText.Max(option => option.icon.Width); int itemIndex = 0; int leftY = currentY; int rightY = currentY; foreach (var (icon, label, _) in sortedIconsWithText) { bool isLeftColumn = (itemIndex % 2 == 0); int xPosition = isLeftColumn ? leftColumnPositionX : rightColumnPositionX; int yPosition = isLeftColumn ? leftY : rightY; var iconPanel = new GameInformationIconPanel(WindowManager, icon, label, maxIconWidth); iconPanel.ClientRectangle = new Rectangle(xPosition, yPosition, columnWidth, legendIconHeight); pnlGameOptions.AddChild(iconPanel); if (isLeftColumn) leftY += legendIconHeight + legendIconPadding; else rightY += legendIconHeight + legendIconPadding; itemIndex++; } currentY = Math.Max(leftY, rightY); } pnlGameOptions.Height = currentY + legendPadding; pnlGameOptions.Visible = true; pnlIconLegend.ClientRectangle = new Rectangle(pnlIconLegend.X, pnlGameOptions.Bottom, pnlIconLegend.Width, pnlIconLegend.Height); } private void SetLegendInfo(GenericHostedGame game) { ClearLegendIconPanel(); var icons = new List<(Texture2D, string)>(); if (game.Locked) icons.Add((txLockedGame, "Game is locked".L10N("Client:Main:LockedGame"))); if (game.Passworded) icons.Add((txPasswordedGame, "Game is passworded".L10N("Client:Main:PasswordedGame"))); if (game.Incompatible) icons.Add((txIncompatibleGame, "Incompatible client version".L10N("Client:Main:IncompatibleGame"))); if (icons.Count == 0) { pnlIconLegend.Visible = false; UpdatePanelHeight(); return; } var divider = CreateDivider(0); pnlIconLegend.AddChild(divider); int currentY = divider.Bottom + legendPadding; foreach (var (icon, label) in icons) { var iconPanel = new GameInformationIconPanel(WindowManager, icon, label); iconPanel.ClientRectangle = new Rectangle(leftColumnPositionX, currentY, pnlIconLegend.Width, legendIconHeight); pnlIconLegend.AddChild(iconPanel); currentY += legendIconHeight; } pnlIconLegend.Height = currentY + legendPadding; pnlIconLegend.Visible = true; UpdatePanelHeight(); } private void UpdatePanelHeight() { int bottomMostY = initialPanelHeight; if (pnlGameOptions.Visible && pnlGameOptions.Bottom > bottomMostY) bottomMostY = pnlGameOptions.Bottom; if (pnlIconLegend.Visible && pnlIconLegend.Bottom > bottomMostY) bottomMostY = pnlIconLegend.Bottom; ClientRectangle = new Rectangle(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, bottomMostY); } private XNAPanel CreateDivider(int y, int height = 1) { var dividerPanel = new XNAPanel(WindowManager); dividerPanel.DrawBorders = true; dividerPanel.ClientRectangle = new Rectangle(0, y, ClientRectangle.Width, height); return dividerPanel; } private void ClearLegendIconPanel() { foreach (XNAControl xnaControl in pnlIconLegend.Children.ToList()) pnlIconLegend.RemoveChild(xnaControl); } public void ClearInfo() { lblGameMode.Visible = false; lblMap.Visible = false; lblGameVersion.Visible = false; lblHost.Visible = false; lblPing.Visible = false; lblPlayers.Visible = false; lblGameInformation.Visible = false; lblSkillLevel.Visible = false; foreach (XNALabel label in lblPlayerNames) label.Visible = false; if (mapPreviewTexture != null && mapPreviewTextureNeedsDispose) { Debug.Assert(!mapPreviewTexture.IsDisposed, "mapPreviewTexture should not be disposed before this call"); mapPreviewTexture.Dispose(); mapPreviewTexture = null; mapPreviewTextureNeedsDispose = false; } } public override void Draw(GameTime gameTime) { if (Alpha > 0.0f) { base.Draw(gameTime); if (game != null && mapPreviewTexture != null) RenderMapPreview(); } } private void RenderMapPreview() { // Calculate map preview area based on right half of ClientRectangle double xRatio = (ClientRectangle.Width / 2 - mapPreviewHorizontalMargin) / (double)mapPreviewTexture.Width; double yRatio = (ClientRectangle.Height - mapPreviewVerticalMargin) / (double)mapPreviewTexture.Height; double ratio = Math.Min(xRatio, yRatio); // Choose the smaller ratio for scaling int textureWidth = (int)(mapPreviewTexture.Width * ratio); int textureHeight = (int)(mapPreviewTexture.Height * ratio); // Apply max height constraint if (textureHeight > maxPreviewHeight) { ratio = maxPreviewHeight / (double)mapPreviewTexture.Height; textureHeight = maxPreviewHeight; textureWidth = (int)(mapPreviewTexture.Width * ratio); // Recalculate width to maintain aspect ratio } int texturePositionX = rightColumnPositionX + (ClientRectangle.Width / 2 - textureWidth) / 2; // Center in the right column int texturePositionY = mapPreviewPositionY; DrawTexture( mapPreviewTexture, new Rectangle(texturePositionX, texturePositionY, textureWidth, textureHeight), Color.White ); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameListBox.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using ClientCore; using ClientCore.Enums; using ClientCore.Extensions; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { /// /// A list box for listing hosted games. /// public class GameListBox : XNAListBox { private const int GAME_REFRESH_RATE = 1; private const int ICON_MARGIN = 2; private static string LOADED_GAME_TEXT => " (" + "Loaded Game".L10N("Client:Main:LoadedGame") + ")"; private readonly string[] SkillLevelOptions; public GameListBox(WindowManager windowManager, MapLoader mapLoader, string localGameIdentifier, GameLobbyBase gameLobby = null, Predicate gameMatchesFilter = null) : base(windowManager) { this.mapLoader = mapLoader; this.localGameIdentifier = localGameIdentifier; this.gameLobby = gameLobby; GameMatchesFilter = gameMatchesFilter; SkillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(','); } private List txSkillLevelIcons = new(); private int loadedGameTextWidth; public List HostedGames = new(); public double GameLifetime { get; set; } = 35.0; /// /// A predicate for setting a filter expression for displayed games. /// private Predicate GameMatchesFilter { get; } private Texture2D txLockedGame; private Texture2D txIncompatibleGame; private Texture2D txPasswordedGame; private string localGameIdentifier; private MapLoader mapLoader; private GameLobbyBase gameLobby; private GameInformationPanel panelGameInformation; private TimeSpan timeSinceGameRefresh; private Color hoverOnGameColor; /// /// Removes a game from the list. /// /// The index of the game to remove. public void RemoveGame(int index) { HostedGames.RemoveAt(index); Refresh(); } /// /// Compares each listed XNAListBoxItem item in the GameListBox to the refernece XNAListBoxItem item for equality. /// /// The XNAListBoxItem to compare against /// bool private static Predicate GameListMatch(XNAListBoxItem referencedItem) => listedItem => { var referencedGame = (GenericHostedGame)referencedItem?.Tag; var listedGame = (GenericHostedGame)listedItem?.Tag; if (referencedGame == null || listedGame == null) return false; return referencedGame.Equals(listedGame); }; /// /// Refreshes game information in the game list box. /// public void Refresh() { var selectedItem = SelectedItem; var hoveredItem = HoveredItem; Clear(); GetSortedAndFilteredGames() .ToList() .ForEach(AddGameToList); if (selectedItem != null) SelectedIndex = Items.FindIndex(GameListMatch(selectedItem)); if (hoveredItem != null) HoveredIndex = Items.FindIndex(GameListMatch(hoveredItem)); ShowGamePanelInfoForIndex(IsValidGameIndex(SelectedIndex) ? SelectedIndex : HoveredIndex); } /// /// Adds a game to the game list. /// /// The game to add. public void AddGame(GenericHostedGame game) { HostedGames.Add(game); // Early notify the map preview cache mapLoader.PrefetchCachedPreviewImageFromMap(mapLoader.FindMapByHash(game.MapHash)); Refresh(); } private IEnumerable GetSortedAndFilteredGames() { var sortedGames = GetSortedGames(); return GameMatchesFilter == null ? sortedGames : sortedGames.Where(hg => GameMatchesFilter(hg)); } private IEnumerable GetSortedGames() { var sortedGames = HostedGames .OrderBy(hg => hg.Locked) .ThenBy(hg => string.Equals(hg.Game.InternalName, localGameIdentifier, StringComparison.InvariantCultureIgnoreCase)) .ThenBy(hg => hg.GameVersion != ProgramConstants.GAME_VERSION) .ThenBy(hg => hg.Passworded); switch ((SortDirection)UserINISettings.Instance.SortState.Value) { case SortDirection.Asc: sortedGames = sortedGames.ThenBy(hg => hg.RoomName); break; case SortDirection.Desc: sortedGames = sortedGames.ThenByDescending(hg => hg.RoomName); break; } return sortedGames; } /// /// Sorts and refreshes the game information in the game list box. /// public void SortAndRefreshHostedGames() { Refresh(); } public void ClearGames() { Clear(); HostedGames.Clear(); } public override void Initialize() { base.Initialize(); txLockedGame = AssetLoader.LoadTexture("lockedgame.png"); txIncompatibleGame = AssetLoader.LoadTexture("incompatible.png"); txPasswordedGame = AssetLoader.LoadTexture("passwordedgame.png"); panelGameInformation = new GameInformationPanel(WindowManager, mapLoader, gameLobby); panelGameInformation.Name = nameof(panelGameInformation); panelGameInformation.BackgroundTexture = AssetLoader.LoadTexture("cncnetlobbypanelbg.png"); panelGameInformation.DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; panelGameInformation.Initialize(); panelGameInformation.ClearInfo(); panelGameInformation.Disable(); panelGameInformation.InputEnabled = false; panelGameInformation.Alpha = 0f; Parent.AddChild(panelGameInformation); // make this a child of our parent so it's not drawn on our rendertarget SelectedIndexChanged += GameListBox_SelectedIndexChanged; HoveredIndexChanged += GameListBox_HoveredIndexChanged; hoverOnGameColor = AssetLoader.GetColorFromString( ClientConfiguration.Instance.HoverOnGameColor); loadedGameTextWidth = (int)Renderer.GetTextDimensions(LOADED_GAME_TEXT, FontIndex).X; InitSkillLevelIcons(); } private void InitSkillLevelIcons() { for (int i = 0; i < SkillLevelOptions.Length; i++) { string fileName = $"skillLevel{i}.png"; txSkillLevelIcons.Add(AssetLoader.AssetExists(fileName) ? AssetLoader.LoadTexture(fileName) : null); } } private bool IsValidGameIndex(int index) { return index >= 0 && index < Items.Count; } private void ShowGamePanelInfoForIndex(int index) { if (!IsValidGameIndex(index)) { panelGameInformation.AlphaRate = -0.5f; return; } panelGameInformation.Enable(); panelGameInformation.X = Right; panelGameInformation.Y = Y; panelGameInformation.AlphaRate = 0.5f; var hostedGame = (GenericHostedGame)Items[index].Tag; panelGameInformation.SetInfo(hostedGame); } private void GameListBox_SelectedIndexChanged(object sender, EventArgs e) { ShowGamePanelInfoForIndex(SelectedIndex); } private void GameListBox_HoveredIndexChanged(object sender, EventArgs e) { if (!IsValidGameIndex(SelectedIndex)) ShowGamePanelInfoForIndex(HoveredIndex); } private (List leftIcons, List rightIcons) GetGameOptionIcons(GenericHostedGame game) { var leftIcons = new List(); var rightIcons = new List(); if (gameLobby == null || game is not HostedCnCNetGame cncnetGame) return (leftIcons, rightIcons); if (cncnetGame.BroadcastedGameOptionValues == null || cncnetGame.BroadcastedGameOptionValues.Length == 0) return (leftIcons, rightIcons); var broadcastableSettings = gameLobby.GetBroadcastableSettings(); for (int i = 0; i < broadcastableSettings.Count && i < cncnetGame.BroadcastedGameOptionValues.Length; i++) { var setting = broadcastableSettings[i]; int value = cncnetGame.BroadcastedGameOptionValues[i]; if (setting is GameLobbyCheckBox checkbox && checkbox.ShowInGameList) { string iconName = value != 0 ? checkbox.EnabledIcon : checkbox.DisabledIcon; if (string.IsNullOrEmpty(iconName)) continue; Texture2D icon = AssetLoader.LoadTexture(iconName); if (icon != null) { if (checkbox.ShowInGameListOnRight) rightIcons.Add(icon); else leftIcons.Add(icon); } } else if (setting is GameLobbyDropDown dropdown && dropdown.ShowInGameList) { // Use the icon for the selected value if (value >= 0 && value < dropdown.Items.Count) { Texture2D icon = dropdown.Items[value].Texture; if (icon != null) { if (dropdown.ShowInGameListOnRight) rightIcons.Add(icon); else leftIcons.Add(icon); } } } } return (leftIcons, rightIcons); } private void AddGameToList(GenericHostedGame hg) { int lgTextWidth = hg.IsLoadedGame ? loadedGameTextWidth : 0; var (leftIcons, rightIcons) = GetGameOptionIcons(hg); int leftIconsWidth = leftIcons.Count > 0 ? (leftIcons.Sum(icon => icon.Width) + (leftIcons.Count * ICON_MARGIN)) : 0; int rightIconsWidth = rightIcons.Count > 0 ? (rightIcons.Sum(icon => icon.Width) + (rightIcons.Count * ICON_MARGIN)) : 0; bool showGameIcon = ClientConfiguration.Instance.ShowGameIconInGameList || hg.Game.InternalName != localGameIdentifier.ToLower(); int gameTextureWidth = showGameIcon ? hg.Game.Texture.Width : 0; int skillLevelIconWidth = 0; if (txSkillLevelIcons[hg.SkillLevel] != null) skillLevelIconWidth = txSkillLevelIcons[hg.SkillLevel].Width; int maxTextWidth = Width - gameTextureWidth - (hg.Incompatible ? txIncompatibleGame.Width : 0) - (hg.Locked ? txLockedGame.Width : 0) - (hg.Passworded ? txPasswordedGame.Width : 0) - skillLevelIconWidth - leftIconsWidth - rightIconsWidth - (ICON_MARGIN * 3) - GetScrollBarWidth() - lgTextWidth; var lbItem = new XNAListBoxItem(); lbItem.Tag = hg; lbItem.Text = Renderer.GetStringWithLimitedWidth(Renderer.GetSafeString( hg.RoomName, FontIndex), FontIndex, maxTextWidth); if (hg.Game.InternalName != localGameIdentifier.ToLower()) lbItem.TextColor = UISettings.ActiveSettings.TextColor; //else // made unnecessary by new Rampastring.XNAUI // lbItem.TextColor = UISettings.ActiveSettings.AltColor; if (hg.Incompatible || hg.Locked) { lbItem.TextColor = Color.Gray; } AddItem(lbItem); } public override void Update(GameTime gameTime) { timeSinceGameRefresh += gameTime.ElapsedGameTime; if (timeSinceGameRefresh.TotalSeconds > GAME_REFRESH_RATE) { for (int i = 0; i < HostedGames.Count; i++) { if (DateTime.Now - HostedGames[i].LastRefreshTime > TimeSpan.FromSeconds(GameLifetime)) { HostedGames.RemoveAt(i); i--; } } Refresh(); timeSinceGameRefresh = TimeSpan.Zero; } base.Update(gameTime); } public override void Draw(GameTime gameTime) { DrawPanel(); int height = 2; for (int i = TopIndex; i < Items.Count; i++) { var lbItem = Items[i]; if (height + lbItem.TextLines.Count * LineHeight > Height) break; int x = TextBorderDistance; bool scrollBarDrawn = ScrollBar.IsDrawn() && EnableScrollbar; int drawnWidth = !scrollBarDrawn || DrawSelectionUnderScrollbar ? Width - 2 : Width - 2 - ScrollBar.Width; if (i == SelectedIndex) { FillRectangle( new Rectangle(1, height, drawnWidth, lbItem.TextLines.Count * LineHeight), FocusColor); } else if (i == HoveredIndex) { FillRectangle( new Rectangle(1, height, drawnWidth, lbItem.TextLines.Count * LineHeight), hoverOnGameColor); } var hostedGame = (GenericHostedGame)lbItem.Tag; // left-side game option icons var (leftIcons, rightIcons) = GetGameOptionIcons(hostedGame); foreach (var icon in leftIcons) { DrawTexture(icon, new Rectangle(x, height, icon.Width, icon.Height), Color.White); x += icon.Width + ICON_MARGIN; } bool showGameIcon = ClientConfiguration.Instance.ShowGameIconInGameList || hostedGame.Game.InternalName != localGameIdentifier.ToLower(); if (showGameIcon) { DrawTexture(hostedGame.Game.Texture, new Rectangle(x, height, hostedGame.Game.Texture.Width, hostedGame.Game.Texture.Height), Color.White); x += hostedGame.Game.Texture.Width + ICON_MARGIN; } if (hostedGame.Locked) { DrawTexture(txLockedGame, new Rectangle(x, height, txLockedGame.Width, txLockedGame.Height), Color.White); x += txLockedGame.Width + ICON_MARGIN; } if (hostedGame.Incompatible) { DrawTexture(txIncompatibleGame, new Rectangle(x, height, txIncompatibleGame.Width, txIncompatibleGame.Height), Color.White); x += txIncompatibleGame.Width + ICON_MARGIN; } // right-side icons (right game option icons, then password, then skill level) int rightX = Width - TextBorderDistance - (scrollBarDrawn ? ScrollBar.Width : 0); // right-side game option icons (drawn first, from right to left) for (int iconIndex = rightIcons.Count - 1; iconIndex >= 0; iconIndex--) { var icon = rightIcons[iconIndex]; rightX -= icon.Width; DrawTexture(icon, new Rectangle(rightX, height, icon.Width, icon.Height), Color.White); rightX -= ICON_MARGIN; } // password icon if (hostedGame.Passworded) { rightX -= txPasswordedGame.Width; DrawTexture(txPasswordedGame, new Rectangle(rightX, height, txPasswordedGame.Width, txPasswordedGame.Height), Color.White); rightX -= ICON_MARGIN; } // skill level icon (shown even if passworded) Texture2D txSkillLevelIcon = txSkillLevelIcons[hostedGame.SkillLevel]; if (txSkillLevelIcon != null) { rightX -= txSkillLevelIcon.Width; DrawTexture(txSkillLevelIcon, new Rectangle(rightX, height, txSkillLevelIcon.Width, txSkillLevelIcon.Height), Color.White); } var text = lbItem.Text; if (hostedGame.IsLoadedGame) text = lbItem.Text + LOADED_GAME_TEXT; x += lbItem.TextXPadding; DrawStringWithShadow(text, FontIndex, new Vector2(x, height), lbItem.TextColor); height += LineHeight; } if (DrawBorders) DrawPanelBorders(); DrawChildren(gameTime); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs ================================================ using ClientCore; using ClientCore.Statistics; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.Multiplayer; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; namespace DTAClient.DXGUI.Multiplayer { /// /// An abstract base class for a multiplayer game loading lobby. /// public abstract class GameLoadingLobbyBase : XNAWindow, ISwitchable { public GameLoadingLobbyBase(WindowManager windowManager, DiscordHandler discordHandler) : base(windowManager) { this.discordHandler = discordHandler; } public event EventHandler GameLeft; /// /// The list of players in the current saved game. /// protected List SGPlayers = new List(); /// /// The list of players in the game lobby. /// protected List Players = new List(); protected bool IsHost = false; protected DiscordHandler discordHandler; protected XNAClientDropDown ddSavedGame; protected ChatListBox lbChatMessages; protected XNATextBox tbChatInput; protected EnhancedSoundEffect sndGetReadySound; protected EnhancedSoundEffect sndJoinSound; protected EnhancedSoundEffect sndLeaveSound; protected EnhancedSoundEffect sndMessageSound; protected XNALabel lblDescription; protected XNAPanel panelPlayers; protected XNALabel[] lblPlayerNames; private XNALabel lblMapName; protected XNALabel lblMapNameValue; private XNALabel lblGameMode; protected XNALabel lblGameModeValue; private XNALabel lblSavedGameTime; protected XNAClientButton btnLoadGame; protected XNAClientButton btnLeaveGame; private List MPColors = new List(); private string loadedGameID; private bool isSettingUp = false; private FileSystemWatcher fsw; private int uniqueGameId = 0; private DateTime gameLoadTime; public override void Initialize() { Name = "GameLoadingLobby"; ClientRectangle = new Rectangle(0, 0, 590, 510); BackgroundTexture = AssetLoader.LoadTexture("loadmpsavebg.png"); lblDescription = new XNALabel(WindowManager); lblDescription.Name = nameof(lblDescription); lblDescription.ClientRectangle = new Rectangle(12, 12, 0, 0); lblDescription.Text = "Wait for all players to join and get ready, then click Load Game to load the saved multiplayer game.".L10N("Client:Main:LobbyInitialTip"); panelPlayers = new XNAPanel(WindowManager); panelPlayers.ClientRectangle = new Rectangle(12, 32, 373, 125); panelPlayers.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); panelPlayers.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; AddChild(lblDescription); AddChild(panelPlayers); lblPlayerNames = new XNALabel[8]; for (int i = 0; i < 8; i++) { XNALabel lblPlayerName = new XNALabel(WindowManager); lblPlayerName.Name = nameof(lblPlayerName) + i; if (i < 4) lblPlayerName.ClientRectangle = new Rectangle(9, 9 + 30 * i, 0, 0); else lblPlayerName.ClientRectangle = new Rectangle(190, 9 + 30 * (i - 4), 0, 0); lblPlayerName.Text = string.Format("Player {0}".L10N("Client:Main:PlayerX"), i) + " "; panelPlayers.AddChild(lblPlayerName); lblPlayerNames[i] = lblPlayerName; } lblMapName = new XNALabel(WindowManager); lblMapName.Name = nameof(lblMapName); lblMapName.FontIndex = 1; lblMapName.ClientRectangle = new Rectangle(panelPlayers.Right + 12, panelPlayers.Y, 0, 0); lblMapName.Text = "MAP:".L10N("Client:Main:MapLabel"); lblMapNameValue = new XNALabel(WindowManager); lblMapNameValue.Name = nameof(lblMapNameValue); lblMapNameValue.ClientRectangle = new Rectangle(lblMapName.X, lblMapName.Y + 18, 0, 0); lblMapNameValue.Text = "Map name".L10N("Client:Main:MapName"); lblGameMode = new XNALabel(WindowManager); lblGameMode.Name = nameof(lblGameMode); lblGameMode.ClientRectangle = new Rectangle(lblMapName.X, panelPlayers.Y + 40, 0, 0); lblGameMode.FontIndex = 1; lblGameMode.Text = "GAME MODE:".L10N("Client:Main:GameMode"); lblGameModeValue = new XNALabel(WindowManager); lblGameModeValue.Name = nameof(lblGameModeValue); lblGameModeValue.ClientRectangle = new Rectangle(lblGameMode.X, lblGameMode.Y + 18, 0, 0); lblGameModeValue.Text = "Game mode".L10N("Client:Main:GameModeValueText"); lblSavedGameTime = new XNALabel(WindowManager); lblSavedGameTime.Name = nameof(lblSavedGameTime); lblSavedGameTime.ClientRectangle = new Rectangle(lblMapName.X, panelPlayers.Bottom - 40, 0, 0); lblSavedGameTime.FontIndex = 1; lblSavedGameTime.Text = "SAVED GAME:".L10N("Client:Main:SavedGame"); ddSavedGame = new XNAClientDropDown(WindowManager); ddSavedGame.Name = nameof(ddSavedGame); ddSavedGame.ClientRectangle = new Rectangle(lblSavedGameTime.X, panelPlayers.Bottom - 21, Width - lblSavedGameTime.X - 12, 21); ddSavedGame.SelectedIndexChanged += DdSavedGame_SelectedIndexChanged; lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = nameof(lbChatMessages); lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbChatMessages.ClientRectangle = new Rectangle(12, panelPlayers.Bottom + 12, Width - 24, Height - panelPlayers.Bottom - 12 - 29 - 34); tbChatInput = new XNATextBox(WindowManager); tbChatInput.Name = nameof(tbChatInput); tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X, lbChatMessages.Bottom + 3, lbChatMessages.Width, 19); tbChatInput.MaximumTextLength = 200; tbChatInput.EnterPressed += TbChatInput_EnterPressed; btnLoadGame = new XNAClientButton(WindowManager); btnLoadGame.Name = nameof(btnLoadGame); btnLoadGame.ClientRectangle = new Rectangle(lbChatMessages.X, tbChatInput.Bottom + 6, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLoadGame.Text = "Load Game".L10N("Client:Main:LoadGame"); btnLoadGame.LeftClick += BtnLoadGame_LeftClick; btnLeaveGame = new XNAClientButton(WindowManager); btnLeaveGame.Name = nameof(btnLeaveGame); btnLeaveGame.ClientRectangle = new Rectangle(Width - 145, btnLoadGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLeaveGame.Text = "Leave Game".L10N("Client:Main:LeaveGame"); btnLeaveGame.LeftClick += BtnLeaveGame_LeftClick; AddChild(lblMapName); AddChild(lblMapNameValue); AddChild(lblGameMode); AddChild(lblGameModeValue); AddChild(lblSavedGameTime); AddChild(lbChatMessages); AddChild(tbChatInput); AddChild(btnLoadGame); AddChild(btnLeaveGame); AddChild(ddSavedGame); base.Initialize(); sndJoinSound = new EnhancedSoundEffect("joingame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyJoinCooldown); sndLeaveSound = new EnhancedSoundEffect("leavegame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyLeaveCooldown); sndMessageSound = new EnhancedSoundEffect("message.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundMessageCooldown); sndGetReadySound = new EnhancedSoundEffect("getready.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyGetReadyCooldown); MPColors = MultiplayerColor.LoadColors(); WindowManager.CenterControlOnScreen(this); if (SavedGameManager.AreSavedGamesAvailable()) { fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Saved Games"), "*.NET"); fsw.EnableRaisingEvents = false; fsw.Created += fsw_Created; fsw.Changed += fsw_Created; } } /// /// Updates Discord Rich Presence with actual information. /// /// Whether to restart the "Elapsed" timer or not protected abstract void UpdateDiscordPresence(bool resetTimer = false); /// /// Resets Discord Rich Presence to default state. /// protected void ResetDiscordPresence() => discordHandler.UpdatePresence(); private void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGame(); protected virtual void LeaveGame() { GameLeft?.Invoke(this, EventArgs.Empty); ResetDiscordPresence(); } private void fsw_Created(object sender, FileSystemEventArgs e) => AddCallback(new Action(HandleFSWEvent), e); private void HandleFSWEvent(FileSystemEventArgs e) { Logger.Log("FSW Event: " + e.FullPath); if (Path.GetFileName(e.FullPath) == "SAVEGAME.NET") { SavedGameManager.RenameSavedGame(); } } private void BtnLoadGame_LeftClick(object sender, EventArgs e) { if (!IsHost) { RequestReadyStatus(); return; } if (Players.Find(p => !p.Ready) != null) { GetReadyNotification(); return; } if (Players.Count != SGPlayers.Count) { NotAllPresentNotification(); return; } HostStartGame(); } protected abstract void RequestReadyStatus(); protected virtual void GetReadyNotification() { AddNotice("The game host wants to load the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyPlease")); if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) sndGetReadySound.Play(); #if WINFORMS WindowManager.FlashWindow(); #endif } protected virtual void NotAllPresentNotification() => AddNotice("You cannot load the game before all players are present.".L10N("Client:Main:NotAllPresent")); protected abstract void HostStartGame(); protected void LoadGame() { FileInfo spawnFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "spawn.ini"); spawnFileInfo.Delete(); File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini"), spawnFileInfo.FullName); IniFile spawnIni = new IniFile(spawnFileInfo.FullName); int sgIndex = (ddSavedGame.Items.Count - 1) - ddSavedGame.SelectedIndex; spawnIni.SetStringValue("Settings", "SaveGameName", string.Format("SVGM_{0}.NET", sgIndex.ToString("D3"))); spawnIni.SetBooleanValue("Settings", "LoadSaveGame", true); PlayerInfo localPlayer = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (localPlayer == null) return; spawnIni.SetIntValue("Settings", "Port", localPlayer.Port); for (int i = 1; i < Players.Count; i++) { string otherName = spawnIni.GetStringValue("Other" + i, "Name", string.Empty); if (string.IsNullOrEmpty(otherName)) continue; PlayerInfo otherPlayer = Players.Find(p => p.Name == otherName); if (otherPlayer == null) continue; spawnIni.SetStringValue("Other" + i, "Ip", otherPlayer.IPAddress); spawnIni.SetIntValue("Other" + i, "Port", otherPlayer.Port); } WriteSpawnIniAdditions(spawnIni); spawnIni.WriteIniFile(); FileInfo spawnMapFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "spawnmap.ini"); spawnMapFileInfo.Delete(); using (var spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName)) { spawnMapStreamWriter.WriteLine("[Map]"); spawnMapStreamWriter.WriteLine("Size=0,0,50,50"); spawnMapStreamWriter.WriteLine("LocalSize=0,0,50,50"); spawnMapStreamWriter.WriteLine(); } gameLoadTime = DateTime.Now; GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; GameProcessLogic.StartGameProcess(WindowManager); fsw.EnableRaisingEvents = true; UpdateDiscordPresence(true); } private void SharedUILogic_GameProcessExited() => AddCallback(new Action(HandleGameProcessExited), null); protected virtual void HandleGameProcessExited() { fsw.EnableRaisingEvents = false; GameProcessLogic.GameProcessExited -= SharedUILogic_GameProcessExited; var matchStatistics = StatisticsManager.Instance.GetMatchWithGameID(uniqueGameId); if (matchStatistics != null) { int oldLength = matchStatistics.LengthInSeconds; int newLength = matchStatistics.LengthInSeconds + (int)(DateTime.Now - gameLoadTime).TotalSeconds; matchStatistics.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, true); matchStatistics.LengthInSeconds = newLength; StatisticsManager.Instance.SaveDatabase(); } UpdateDiscordPresence(true); } protected virtual void WriteSpawnIniAdditions(IniFile spawnIni) { // Do nothing by default } protected void AddNotice(string notice) => AddNotice(notice, Color.White); protected abstract void AddNotice(string message, Color color); /// /// Refreshes the UI based on the latest saved game /// and information in the saved spawn.ini file, as well /// as information on whether the local player is the host of the game. /// public virtual void Refresh(bool isHost) { isSettingUp = true; IsHost = isHost; SGPlayers.Clear(); Players.Clear(); ddSavedGame.Items.Clear(); lbChatMessages.Clear(); lbChatMessages.TopIndex = 0; ddSavedGame.AllowDropDown = isHost; btnLoadGame.Text = isHost ? "Load Game".L10N("Client:Main:ButtonLoadGame") : "I'm Ready".L10N("Client:Main:ButtonGetReady"); IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini")); loadedGameID = spawnSGIni.GetStringValue("Settings", "GameID", "0"); lblMapNameValue.Tag = spawnSGIni.GetStringValue("Settings", "UIMapName", string.Empty); lblMapNameValue.Text = ((string)lblGameModeValue.Tag).L10N($"INI:Maps:{spawnSGIni.GetStringValue("Settings", "MapID", string.Empty)}:Description"); lblGameModeValue.Tag = spawnSGIni.GetStringValue("Settings", "UIGameMode", string.Empty); lblGameModeValue.Text = ((string)lblGameModeValue.Tag).L10N($"INI:GameModes:{(string)lblGameModeValue.Tag}:UIName"); uniqueGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); int playerCount = spawnSGIni.GetIntValue("Settings", "PlayerCount", 0); SavedGamePlayer localPlayer = new SavedGamePlayer(); localPlayer.Name = ProgramConstants.PLAYERNAME; localPlayer.ColorIndex = MPColors.FindIndex( c => c.GameColorIndex == spawnSGIni.GetIntValue("Settings", "Color", 0)); SGPlayers.Add(localPlayer); for (int i = 1; i < playerCount; i++) { string sectionName = "Other" + i; SavedGamePlayer sgPlayer = new SavedGamePlayer(); sgPlayer.Name = spawnSGIni.GetStringValue(sectionName, "Name", "Unknown player".L10N("Client:Main:UnknownPlayer")); sgPlayer.ColorIndex = MPColors.FindIndex( c => c.GameColorIndex == spawnSGIni.GetIntValue(sectionName, "Color", 0)); SGPlayers.Add(sgPlayer); } for (int i = 0; i < SGPlayers.Count; i++) { lblPlayerNames[i].Enabled = true; lblPlayerNames[i].Visible = true; } for (int i = SGPlayers.Count; i < 8; i++) { lblPlayerNames[i].Enabled = false; lblPlayerNames[i].Visible = false; } List timestamps = SavedGameManager.GetSaveGameTimestamps(); timestamps.Reverse(); // Most recent saved game first timestamps.ForEach(ts => ddSavedGame.AddItem(ts)); if (ddSavedGame.Items.Count > 0) ddSavedGame.SelectedIndex = 0; CopyPlayerDataToUI(); isSettingUp = false; } protected void CopyPlayerDataToUI() { for (int i = 0; i < SGPlayers.Count; i++) { SavedGamePlayer sgPlayer = SGPlayers[i]; PlayerInfo pInfo = Players.Find(p => p.Name == SGPlayers[i].Name); XNALabel playerLabel = lblPlayerNames[i]; if (pInfo == null) { playerLabel.RemapColor = Color.Gray; playerLabel.Text = sgPlayer.Name + " " + "(Not present)".L10N("Client:Main:NotPresentSuffix"); continue; } playerLabel.RemapColor = sgPlayer.ColorIndex > -1 ? MPColors[sgPlayer.ColorIndex].XnaColor : Color.White; playerLabel.Text = pInfo.Ready ? sgPlayer.Name : sgPlayer.Name + " " + "(Not Ready)".L10N("Client:Main:NotReadySuffix"); } } protected virtual string GetIPAddressForPlayer(PlayerInfo pInfo) => "0.0.0.0"; private void DdSavedGame_SelectedIndexChanged(object sender, EventArgs e) { if (!IsHost) return; for (int i = 1; i < Players.Count; i++) Players[i].Ready = false; CopyPlayerDataToUI(); if (!isSettingUp) BroadcastOptions(); UpdateDiscordPresence(); } private void TbChatInput_EnterPressed(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbChatInput.Text)) return; SendChatMessage(tbChatInput.Text); tbChatInput.Text = string.Empty; } /// /// Override in a derived class to broadcast player ready statuses and the selected /// saved game to players. /// protected abstract void BroadcastOptions(); protected abstract void SendChatMessage(string message); public override void Draw(GameTime gameTime) { Renderer.FillRectangle(new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY), new Color(0, 0, 0, 255)); base.Draw(gameTime); } public void SwitchOn() => Enable(); public void SwitchOff() => Disable(); public abstract string GetSwitchName(); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/ChatBoxCommand.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby { /// /// A command that can be executed by typing a message starting with / on /// a multiplayer game lobby's chat box. /// public class ChatBoxCommand { public ChatBoxCommand(string command, string description, bool hostOnly, Action action) { Command = command; Description = description; HostOnly = hostOnly; Action = action; } public string Command { get; private set; } public string Description { get; private set; } public bool HostOnly { get; private set; } public Action Action { get; private set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs ================================================ using ClientCore; using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.Domain; using DTAClient.DXGUI.Generic; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers; using DTAClient.Online; using DTAClient.Online.EventArguments; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class CnCNetGameLobby : MultiplayerGameLobby { private const int HUMAN_PLAYER_OPTIONS_LENGTH = 3; private const int AI_PLAYER_OPTIONS_LENGTH = 2; private const double GAME_BROADCAST_INTERVAL = 30.0; private const double GAME_BROADCAST_ACCELERATION = 10.0; private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; private const string MAP_SHARING_FAIL_MESSAGE = "MAPFAIL"; private const string MAP_SHARING_DOWNLOAD_REQUEST = "MAPOK"; private const string MAP_SHARING_UPLOAD_REQUEST = "MAPREQ"; private const string MAP_SHARING_DISABLED_MESSAGE = "MAPSDISABLED"; private const string CHEAT_DETECTED_MESSAGE = "CD"; private const string DICE_ROLL_MESSAGE = "DR"; private const string CHANGE_TUNNEL_SERVER_MESSAGE = "CHTNL"; public CnCNetGameLobby( WindowManager windowManager, TopBar topBar, CnCNetManager connectionManager, TunnelHandler tunnelHandler, GameCollection gameCollection, CnCNetUserData cncnetUserData, MapLoader mapLoader, DiscordHandler discordHandler, PrivateMessagingWindow pmWindow, Random random ) : base(windowManager, "MultiplayerGameLobby", topBar, mapLoader, discordHandler, pmWindow, random) { this.connectionManager = connectionManager; localGame = ClientConfiguration.Instance.LocalGame; this.tunnelHandler = tunnelHandler; this.gameCollection = gameCollection; this.cncnetUserData = cncnetUserData; this.pmWindow = pmWindow; this.random = random; gameHostInactiveChecker = ClientConfiguration.Instance.InactiveHostKickEnabled? new GameHostInactiveChecker(WindowManager) : null; ctcpCommandHandlers = new CommandHandlerBase[] { new IntCommandHandler("OR", HandleOptionsRequest), new IntCommandHandler("R", HandleReadyRequest), new StringCommandHandler("PO", ApplyPlayerOptions), new StringCommandHandler(PlayerExtraOptions.CNCNET_MESSAGE_KEY, ApplyPlayerExtraOptions), new StringCommandHandler("GO", ApplyGameOptions), new StringCommandHandler("START", NonHostLaunchGame), new NotificationHandler("AISPECS", HandleNotification, AISpectatorsNotification), new NotificationHandler("GETREADY", HandleNotification, GetReadyNotification), new NotificationHandler("INSFSPLRS", HandleNotification, InsufficientPlayersNotification), new NotificationHandler("TMPLRS", HandleNotification, TooManyPlayersNotification), new NotificationHandler("CLRS", HandleNotification, SharedColorsNotification), new NotificationHandler("SLOC", HandleNotification, SharedStartingLocationNotification), new NotificationHandler("LCKGME", HandleNotification, LockGameNotification), new IntNotificationHandler("NVRFY", HandleIntNotification, NotVerifiedNotification), new IntNotificationHandler("INGM", HandleIntNotification, StillInGameNotification), new StringCommandHandler(MAP_SHARING_UPLOAD_REQUEST, HandleMapUploadRequest), new StringCommandHandler(MAP_SHARING_FAIL_MESSAGE, HandleMapTransferFailMessage), new StringCommandHandler(MAP_SHARING_DOWNLOAD_REQUEST, HandleMapDownloadRequest), new NoParamCommandHandler(MAP_SHARING_DISABLED_MESSAGE, HandleMapSharingBlockedMessage), new NoParamCommandHandler("STRTD", GameStartedNotification), new NoParamCommandHandler("RETURN", ReturnNotification), new IntCommandHandler("TNLPNG", HandleTunnelPing), new StringCommandHandler("FHSH", FileHashNotification), new StringCommandHandler("MM", CheaterNotification), new StringCommandHandler(DICE_ROLL_MESSAGE, HandleDiceRollResult), new NoParamCommandHandler(CHEAT_DETECTED_MESSAGE, HandleCheatDetectedMessage), new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage), new StringCommandHandler("GSETTINGS", ApplyGameLobbySettings) }; MapSharer.MapDownloadFailed += MapSharer_MapDownloadFailed; MapSharer.MapDownloadComplete += MapSharer_MapDownloadComplete; MapSharer.MapUploadFailed += MapSharer_MapUploadFailed; MapSharer.MapUploadComplete += MapSharer_MapUploadComplete; AddChatBoxCommand(new ChatBoxCommand("TUNNELINFO", "View tunnel server information".L10N("Client:Main:TunnelInfoCommand"), false, PrintTunnelServerInformation)); AddChatBoxCommand(new ChatBoxCommand("CHANGETUNNEL", "Change the used CnCNet tunnel server (game host only)".L10N("Client:Main:ChangeTunnelCommand"), true, (s) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServerCommand")))); AddChatBoxCommand(new ChatBoxCommand("DOWNLOADMAP", "Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("Client:Main:DownloadMapCommandDescription"), true, DownloadMapByIdCommand)); } public event EventHandler GameLeft; private TunnelHandler tunnelHandler; private TunnelSelectionWindow tunnelSelectionWindow; private GameLobbySettingsWindow gameLobbySettingsWindow; private XNAClientButton btnChangeTunnel; private XNAClientButton btnGameLobbySettings; private Channel channel; private CnCNetManager connectionManager; private string localGame; private readonly GameHostInactiveChecker gameHostInactiveChecker; private GameCollection gameCollection; private CnCNetUserData cncnetUserData; private readonly PrivateMessagingWindow pmWindow; private GlobalContextMenu globalContextMenu; private string hostName; private CommandHandlerBase[] ctcpCommandHandlers; private IRCColor chatColor; private XNATimerControl gameBroadcastTimer; private int playerLimit; protected override int MaxPlayerCount => playerLimit; private bool closed = false; private int skillLevel = ClientConfiguration.Instance.DefaultSkillLevelIndex; private string gameRoomName; private bool isCustomPassword = false; private string gameFilesHash; private List hostUploadedMaps = new List(); private List chatCommandDownloadedMaps = new List(); private MapSharingConfirmationPanel mapSharingConfirmationPanel; private Random random; /// /// The SHA1 of the latest selected map. /// Used for map sharing. /// private string lastMapSHA1; /// /// The map name of the latest selected map. /// Used for map sharing. /// private string lastMapName; /// /// The game mode of the latest selected map. /// Used for map sharing. /// private string lastGameMode; /// /// Set to true if host has selected invalid tunnel server. /// private bool tunnelErrorMode; public override void Initialize() { IniNameOverride = nameof(CnCNetGameLobby); base.Initialize(); if (gameHostInactiveChecker != null) { MouseMove += (sender, args) => gameHostInactiveChecker.Reset(); gameHostInactiveChecker.CloseEvent += GameHostInactiveChecker_CloseEvent; } btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; btnGameLobbySettings = FindChild(nameof(btnGameLobbySettings), optional: true); btnGameLobbySettings?.LeftClick += BtnGameLobbySettings_LeftClick; gameBroadcastTimer = new XNATimerControl(WindowManager); gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = false; gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed; tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); tunnelSelectionWindow.Initialize(); tunnelSelectionWindow.DrawOrder = 1; tunnelSelectionWindow.UpdateOrder = 1; DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); tunnelSelectionWindow.CenterOnParent(); tunnelSelectionWindow.Disable(); tunnelSelectionWindow.TunnelSelected += TunnelSelectionWindow_TunnelSelected; gameLobbySettingsWindow = new GameLobbySettingsWindow(WindowManager); gameLobbySettingsWindow.Initialize(); gameLobbySettingsWindow.DrawOrder = 1; gameLobbySettingsWindow.UpdateOrder = 1; DarkeningPanel.AddAndInitializeWithControl(WindowManager, gameLobbySettingsWindow); gameLobbySettingsWindow.CenterOnParent(); gameLobbySettingsWindow.Disable(); gameLobbySettingsWindow.SettingsChanged += GameLobbySettingsWindow_SettingsChanged; MapLoader.MapChanged += MapLoader_MapChanged; mapSharingConfirmationPanel = new MapSharingConfirmationPanel(WindowManager); MapPreviewBox.AddChild(mapSharingConfirmationPanel); mapSharingConfirmationPanel.MapDownloadConfirmed += MapSharingConfirmationPanel_MapDownloadConfirmed; WindowManager.AddAndInitializeControl(gameBroadcastTimer); globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow); AddChild(globalContextMenu); MultiplayerNameRightClicked += MultiplayerName_RightClick; PostInitialize(); } private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args) { globalContextMenu.Show(new GlobalContextMenuData() { PlayerName = args.PlayerName, PreventJoinGame = true }, GetCursorPoint()); } private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame(); public void SetUp(Channel channel, bool isHost, int playerLimit, CnCNetTunnel tunnel, string hostName, bool isCustomPassword, int skillLevel) { this.channel = channel; channel.MessageAdded += Channel_MessageAdded; channel.CTCPReceived += Channel_CTCPReceived; channel.UserKicked += Channel_UserKicked; channel.UserQuitIRC += Channel_UserQuitIRC; channel.UserLeft += Channel_UserLeft; channel.UserAdded += Channel_UserAdded; channel.UserNameChanged += Channel_UserNameChanged; channel.UserListReceived += Channel_UserListReceived; this.hostName = hostName; this.playerLimit = playerLimit; this.isCustomPassword = isCustomPassword; this.skillLevel = skillLevel; this.gameRoomName = channel.UIName; if (isHost) { RandomSeed = random.Next(); RefreshMapSelectionUI(); btnChangeTunnel.Enable(); btnGameLobbySettings?.Enable(); StartInactiveCheck(); } else { channel.ChannelModesChanged += Channel_ChannelModesChanged; AIPlayers.Clear(); btnChangeTunnel.Disable(); btnGameLobbySettings?.Disable(); } tunnelHandler.CurrentTunnel = tunnel; tunnelHandler.CurrentTunnelPinged += TunnelHandler_CurrentTunnelPinged; connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; connectionManager.Disconnected += ConnectionManager_Disconnected; Refresh(isHost); } private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) => UpdatePing(); private void GameHostInactiveChecker_CloseEvent(object sender, EventArgs e) => LeaveGameLobby(); public void StartInactiveCheck() { if (isCustomPassword) return; gameHostInactiveChecker?.Start(); } public void StopInactiveCheck() => gameHostInactiveChecker?.Stop(); public void OnJoined() { FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(); gameFilesHash = fhc.GetCompleteHash(); if (IsHost) { connectionManager.SendCustomMessage(new QueuedMessage( string.Format("MODE {0} +klnNs {1} {2}", channel.ChannelName, channel.Password, playerLimit), QueuedMessageType.SYSTEM_MESSAGE, 50)); connectionManager.SendCustomMessage(new QueuedMessage( string.Format("TOPIC {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), QueuedMessageType.SYSTEM_MESSAGE, 50)); gameBroadcastTimer.Enabled = true; gameBroadcastTimer.Start(); gameBroadcastTimer.SetTime(TimeSpan.FromSeconds(INITIAL_GAME_BROADCAST_DELAY)); } else { channel.SendCTCPMessage("FHSH " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); } TopBar.AddPrimarySwitchable(this); TopBar.SwitchToPrimary(); WindowManager.SelectedControl = tbChatInput; ResetAutoReadyCheckbox(); UpdatePing(); UpdateDiscordPresence(true); } private void UpdatePing() { if (tunnelHandler.CurrentTunnel == null) return; channel.SendCTCPMessage("TNLPNG " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); if (pInfo != null) { pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs; UpdatePlayerPingIndicator(pInfo); } } protected override void CopyPlayerDataToUI() { base.CopyPlayerDataToUI(); for (int i = AIPlayers.Count + Players.Count; i < MAX_PLAYER_COUNT; i++) { StatusIndicators[i].SwitchTexture( i < playerLimit ? PlayerSlotState.Empty : PlayerSlotState.Unavailable); } } private void PrintTunnelServerInformation(string s) { if (tunnelHandler.CurrentTunnel == null) { AddNotice("Tunnel server unavailable!".L10N("Client:Main:TunnelUnavailable")); } else { AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("Client:Main:TunnelInfo"), tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official )); } } private void ShowTunnelSelectionWindow(string description) { tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel?.Address); } private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e) { channel.SendCTCPMessage($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", QueuedMessageType.SYSTEM_MESSAGE, 10); HandleTunnelServerChange(e.Tunnel); } private void BtnGameLobbySettings_LeftClick(object sender, EventArgs e) { if (!IsHost) return; string displayPassword = isCustomPassword ? channel.Password : string.Empty; gameLobbySettingsWindow.Open(gameRoomName, playerLimit, skillLevel, displayPassword); } private void GameLobbySettingsWindow_SettingsChanged(object sender, GameLobbySettingsEventArgs e) { if (!IsHost) return; UpdateGameLobbySettings(e.GameRoomName, e.MaxPlayers, e.SkillLevel, e.Password); } private void UpdateGameLobbySettings(string newGameRoomName, int newMaxPlayers, int newSkillLevel, string newPassword) { if (!IsHost) return; bool gameNameChanged = gameRoomName != newGameRoomName; bool maxPlayersChanged = playerLimit != newMaxPlayers; bool skillLevelChanged = skillLevel != newSkillLevel; string currentUserPassword = isCustomPassword ? channel.Password : string.Empty; bool passwordChanged = currentUserPassword != newPassword; // ensure max players isn't less than current player count if (newMaxPlayers < Players.Count + AIPlayers.Count) { AddNotice(string.Format("Cannot reduce maximum players to {0} with {1} players currently in game." .L10N("Client:Main:CannotReduceMaxPlayers"), newMaxPlayers, Players.Count + AIPlayers.Count)); return; } string oldGameRoomName = gameRoomName; bool oldIsCustomPassword = isCustomPassword; gameRoomName = newGameRoomName; channel.UIName = newGameRoomName; playerLimit = newMaxPlayers; skillLevel = newSkillLevel; if (passwordChanged) { // if new password is empty, generate password from channel name string actualNewPassword = newPassword; if (string.IsNullOrEmpty(newPassword)) { actualNewPassword = Utilities.CalculateSHA1ForString(channel.ChannelName).Substring(0, 10); isCustomPassword = false; } else { isCustomPassword = true; } channel.ChangePassword(actualNewPassword, 10); } BroadcastGameLobbySettings(); if (gameNameChanged) { AddNotice(string.Format("Game room name changed from \"{0}\" to \"{1}\"." .L10N("Client:Main:GameNameChanged"), oldGameRoomName, gameRoomName)); } if (maxPlayersChanged) { CopyPlayerDataToUI(); AddNotice(string.Format("Maximum players changed to {0}." .L10N("Client:Main:MaxPlayersChanged"), newMaxPlayers)); } if (skillLevelChanged) { string[] skillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(','); string skillLevelName = skillLevelOptions[newSkillLevel]; string localizedSkillLevel = skillLevelName.L10N($"INI:ClientDefinitions:SkillLevel:{newSkillLevel}"); AddNotice(string.Format("Skill level changed to {0}." .L10N("Client:Main:SkillLevelChanged"), localizedSkillLevel)); } if (passwordChanged) { if (string.IsNullOrEmpty(newPassword)) AddNotice("Password removed from the game.".L10N("Client:Main:PasswordRemoved")); else if (!oldIsCustomPassword) AddNotice("Password added to the game.".L10N("Client:Main:PasswordAdded")); else AddNotice("Password changed.".L10N("Client:Main:PasswordChanged")); } BroadcastGame(); } private void BroadcastGameLobbySettings() { if (!IsHost) return; StringBuilder sb = new StringBuilder("GSETTINGS "); sb.Append(gameRoomName); sb.Append(";"); sb.Append(playerLimit); sb.Append(";"); sb.Append(skillLevel); sb.Append(";"); sb.Append(Convert.ToInt32(isCustomPassword)); channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } private void ApplyGameLobbySettings(string sender, string message) { if (IsHost) return; string[] parts = message.Split(';'); if (parts.Length < 4) return; string newGameRoomName = parts[0]; int newMaxPlayers = Conversions.IntFromString(parts[1], playerLimit); int newSkillLevel = Conversions.IntFromString(parts[2], skillLevel); bool newIsCustomPassword = Convert.ToBoolean(Conversions.IntFromString(parts[3], 0)); bool gameNameChanged = gameRoomName != newGameRoomName; bool maxPlayersChanged = playerLimit != newMaxPlayers; bool skillLevelChanged = skillLevel != newSkillLevel; gameRoomName = newGameRoomName; channel.UIName = newGameRoomName; playerLimit = newMaxPlayers; skillLevel = newSkillLevel; isCustomPassword = newIsCustomPassword; if (gameNameChanged) { AddNotice(string.Format("{0} changed game room name to \"{1}\"." .L10N("Client:Main:HostChangedGameName"), sender, gameRoomName)); } if (maxPlayersChanged) { CopyPlayerDataToUI(); AddNotice(string.Format("{0} changed maximum players to {1}." .L10N("Client:Main:HostChangedMaxPlayers"), sender, newMaxPlayers)); } if (skillLevelChanged) { string[] skillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(','); string skillLevelName = skillLevelOptions[newSkillLevel]; string localizedSkillLevel = skillLevelName.L10N($"INI:ClientDefinitions:SkillLevel:{newSkillLevel}"); AddNotice(string.Format("{0} changed skill level to {1}." .L10N("Client:Main:HostChangedSkillLevel"), sender, localizedSkillLevel)); } } public void ChangeChatColor(IRCColor chatColor) { this.chatColor = chatColor; tbChatInput.TextColor = chatColor.XnaColor; } public override void Clear() { base.Clear(); if (channel != null) { channel.MessageAdded -= Channel_MessageAdded; channel.CTCPReceived -= Channel_CTCPReceived; channel.UserKicked -= Channel_UserKicked; channel.UserQuitIRC -= Channel_UserQuitIRC; channel.UserLeft -= Channel_UserLeft; channel.UserAdded -= Channel_UserAdded; channel.UserNameChanged -= Channel_UserNameChanged; channel.UserListReceived -= Channel_UserListReceived; if (!IsHost) { channel.ChannelModesChanged -= Channel_ChannelModesChanged; } connectionManager.RemoveChannel(channel); } Disable(); PlayerExtraOptionsPanel?.Disable(); connectionManager.ConnectionLost -= ConnectionManager_ConnectionLost; connectionManager.Disconnected -= ConnectionManager_Disconnected; gameBroadcastTimer.Enabled = false; closed = false; tbChatInput.Text = string.Empty; tunnelHandler.CurrentTunnel = null; tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; if (MapLoader != null) MapLoader.MapChanged -= MapLoader_MapChanged; GameLeft?.Invoke(this, EventArgs.Empty); TopBar.RemovePrimarySwitchable(this); ResetDiscordPresence(); } public void LeaveGameLobby() { if (IsHost) { StopInactiveCheck(); closed = true; BroadcastGame(); } Clear(); channel?.Leave(); } private void ConnectionManager_Disconnected(object sender, EventArgs e) => HandleConnectionLoss(); private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => HandleConnectionLoss(); private void HandleConnectionLoss() { Clear(); Disable(); } private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) { Logger.Log("CnCNetGameLobby: Nickname change: " + e.OldUserName + " to " + e.User.Name); int index = Players.FindIndex(p => p.Name == e.OldUserName); if (index > -1) { PlayerInfo player = Players[index]; player.Name = e.User.Name; ddPlayerNames[index].Items[0].Text = player.Name; AddNotice(string.Format("Player {0} changed their name to {1}".L10N("Client:Main:PlayerRename"), e.OldUserName, e.User.Name)); } } protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGameLobby(); protected override void UpdateDiscordPresence(bool resetTimer = false) { if (discordHandler == null) return; PlayerInfo player = FindLocalPlayer(); if (player == null || Map == null || GameMode == null) return; string side = ""; if (ddPlayerSides.Length > Players.IndexOf(player)) side = (string)ddPlayerSides[Players.IndexOf(player)].SelectedItem.Tag; string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings discordHandler.UpdatePresence( Map.UntranslatedName, GameMode.UntranslatedUIName, "Multiplayer", currentState, Players.Count, playerLimit, side, channel.UIName, IsHost, isCustomPassword, Locked, resetTimer); } private void Channel_UserQuitIRC(object sender, UserNameEventArgs e) { RemovePlayer(e.UserName); if (e.UserName == hostName) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); BtnLeaveGame_LeftClick(this, EventArgs.Empty); } else UpdateDiscordPresence(); } private void Channel_UserLeft(object sender, UserNameEventArgs e) { RemovePlayer(e.UserName); if (e.UserName == hostName) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); BtnLeaveGame_LeftClick(this, EventArgs.Empty); } else UpdateDiscordPresence(); } private void Channel_UserKicked(object sender, UserNameEventArgs e) { if (e.UserName == ProgramConstants.PLAYERNAME) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); Clear(); this.Visible = false; this.Enabled = false; return; } int index = Players.FindIndex(p => p.Name == e.UserName); if (index > -1) { Players.RemoveAt(index); CopyPlayerDataToUI(); UpdateDiscordPresence(); ClearReadyStatuses(); } } private void Channel_UserListReceived(object sender, EventArgs e) { if (!IsHost) { if (channel.Users.Find(hostName) == null) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); BtnLeaveGame_LeftClick(this, EventArgs.Empty); } } UpdateDiscordPresence(); } private void Channel_UserAdded(object sender, ChannelUserEventArgs e) { PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name); Players.Add(pInfo); if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) AIPlayers.RemoveAt(AIPlayers.Count - 1); sndJoinSound.Play(); #if WINFORMS WindowManager.FlashWindow(); #endif if (!IsHost) { CopyPlayerDataToUI(); return; } if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) { // Changing the map applies forced settings (co-op sides etc.) to the // new player, and it also sends an options broadcast message //CopyPlayerDataToUI(); This is also called by ChangeMap() ChangeMap(GameModeMap); BroadcastPlayerOptions(); BroadcastPlayerExtraOptions(); UpdateDiscordPresence(); } else { Players[0].Ready = true; CopyPlayerDataToUI(); } if (Players.Count >= playerLimit) { AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); LockGame(); } } private void RemovePlayer(string playerName) { PlayerInfo pInfo = Players.Find(p => p.Name == playerName); if (pInfo != null) { Players.Remove(pInfo); CopyPlayerDataToUI(); // This might not be necessary if (IsHost) BroadcastPlayerOptions(); } sndLeaveSound.Play(); if (IsHost && Locked && !ProgramConstants.IsInGame) { UnlockGame(true); } } private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e) { if (e.ModeString == "+i") { if (Players.Count >= playerLimit) AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); else AddNotice("The game host has locked the game room.".L10N("Client:Main:RoomLockedByHost")); Locked = true; } else if (e.ModeString == "-i") { AddNotice("The game room has been unlocked.".L10N("Client:Main:GameRoomUnlocked")); Locked = false; } } private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) { Logger.Log("CnCNetGameLobby_CTCPReceived"); foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers) { if (cmdHandler.Handle(e.UserName, e.Message)) { UpdateDiscordPresence(); return; } } Logger.Log("Unhandled CTCP command: " + e.Message + " from " + e.UserName); } private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) { if (cncnetUserData.IsIgnored(e.Message.SenderIdent)) { lbChatMessages.AddMessage(new ChatMessage(Color.Silver, string.Format("Message blocked from {0}".L10N("Client:Main:MessageBlockedFromPlayer"), e.Message.SenderName))); } else { lbChatMessages.AddMessage(e.Message); if (e.Message.SenderName != null) sndMessageSound.Play(); } } /// /// Starts the game for the game host. /// protected override void HostLaunchGame() { if (Players.Count > 1) { AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); if (playerPorts.Count < Players.Count) { ShowTunnelSelectionWindow(("An error occured while contacting " + "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); AddNotice(("An error occured while contacting the specified CnCNet " + "tunnel server. Please try using a different tunnel server").L10N("Client:Main:ConnectTunnelError2") + " ", ERROR_MESSAGE_COLOR); return; } StringBuilder sb = new StringBuilder("START "); sb.Append(UniqueGameID); for (int pId = 0; pId < Players.Count; pId++) { Players[pId].Port = playerPorts[pId]; sb.Append(";"); sb.Append(Players[pId].Name); sb.Append(";"); sb.Append(tunnelHandler.CurrentTunnel.Address + ":"); sb.Append(playerPorts[pId]); } channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 10); } else { Logger.Log("One player MP -- starting!"); } cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), gameRoomName); StartGame(); } protected override void RequestPlayerOptions(int side, int color, int start, int team) { byte[] value = new byte[] { (byte)side, (byte)color, (byte)start, (byte)team }; int intValue = BinaryPrimitives.ReadInt32LittleEndian(value); channel.SendCTCPMessage( string.Format("OR {0}", intValue), QueuedMessageType.GAME_SETTINGS_MESSAGE, 6); } protected override void RequestReadyStatus() { if (Map == null || GameMode == null) { AddNotice(("The game host needs to select a different map or " + "you will be unable to participate in the match.").L10N("Client:Main:HostMustReplaceMap")); if (chkAutoReady.Checked) channel.SendCTCPMessage("R 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); return; } PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (pInfo == null) return; int readyState = 0; if (chkAutoReady.Checked) readyState = 2; else if (!pInfo.Ready) readyState = 1; channel.SendCTCPMessage($"R {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); } protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message)); /// /// Handles player option requests received from non-host players. /// private void HandleOptionsRequest(string playerName, int options) { if (!IsHost) return; if (ProgramConstants.IsInGame) return; PlayerInfo pInfo = Players.Find(p => p.Name == playerName); if (pInfo == null) return; byte[] bytes = new byte[sizeof(int)]; BinaryPrimitives.WriteInt32LittleEndian(bytes, options); int side = bytes[0]; int color = bytes[1]; int start = bytes[2]; int team = bytes[3]; if (side < 0 || side > SideCount + RandomSelectorCount) return; if (color < 0 || color > MPColors.Count) return; // Disallowed sides from client, maps, or game modes do not take random selectors into account // So, we need to insert "false" for each random at the beginning of this list AFTER getting them // from client, maps, or game modes. var randomDisallowedSides = new List(RandomSelectorCount); for (int i = 0; i < RandomSelectorCount; i++) randomDisallowedSides.Add(false); var disallowedSides = randomDisallowedSides.Concat(GetDisallowedSides()).ToArray(); if (0 < side && side < SideCount && disallowedSides[side]) return; if (GameModeMap?.CoopInfo != null) { if (GameModeMap.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; if (GameModeMap.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; } if (!(start == 0 || (GameModeMap?.AllowedStartingLocations?.Contains(start) ?? true))) return; if (team < 0 || team > 4) return; if (side != pInfo.SideId || start != pInfo.StartingLocation || team != pInfo.TeamId) { ClearReadyStatuses(); } pInfo.SideId = side; pInfo.ColorId = color; pInfo.StartingLocation = start; pInfo.TeamId = team; CopyPlayerDataToUI(); BroadcastPlayerOptions(); } /// /// Handles "I'm ready" messages received from non-host players. /// private void HandleReadyRequest(string playerName, int readyStatus) { if (!IsHost) return; PlayerInfo pInfo = Players.Find(p => p.Name == playerName); if (pInfo == null) return; pInfo.Ready = readyStatus > 0; pInfo.AutoReady = readyStatus > 1; CopyPlayerDataToUI(); BroadcastPlayerOptions(); } /// /// Broadcasts player options to non-host players. /// protected override void BroadcastPlayerOptions() { // Broadcast player options StringBuilder sb = new StringBuilder("PO "); foreach (PlayerInfo pInfo in Players.Concat(AIPlayers)) { if (pInfo.IsAI) sb.Append(pInfo.AILevel); else sb.Append(pInfo.Name); sb.Append(";"); // Combine the options into one integer to save bandwidth in // cases where the player uses default options (this is common for AI players) // Will hopefully make GameSurge kicking people a bit less common byte[] byteArray = new byte[] { (byte)pInfo.TeamId, (byte)pInfo.StartingLocation, (byte)pInfo.ColorId, (byte)pInfo.SideId, }; int value = BinaryPrimitives.ReadInt32LittleEndian(byteArray); sb.Append(value); sb.Append(";"); if (!pInfo.IsAI) { if (pInfo.AutoReady && !pInfo.IsInGame && !LastMapChangeWasInvalid) sb.Append(2); else sb.Append(Convert.ToInt32(pInfo.Ready)); sb.Append(';'); } } channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); } protected override void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e) { base.PlayerExtraOptions_OptionsChanged(sender, e); BroadcastPlayerExtraOptions(); } protected override void BroadcastPlayerExtraOptions() { if (!IsHost) return; var playerExtraOptions = GetPlayerExtraOptions(); channel.SendCTCPMessage(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); } /// /// Handles player option messages received from the game host. /// private void ApplyPlayerOptions(string sender, string message) { if (sender != hostName) return; Players.Clear(); AIPlayers.Clear(); string[] parts = message.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < parts.Length;) { PlayerInfo pInfo = new PlayerInfo(); string pName = parts[i]; int converted = Conversions.IntFromString(pName, -1); if (converted > -1) { pInfo.IsAI = true; pInfo.AILevel = converted; pInfo.Name = AILevelToName(converted); } else { pInfo.Name = pName; // If we can't find the player from the channel user list, // ignore the player // They've either left the channel or got kicked before the // player options message reached us if (channel.Users.Find(pName) == null) { i += HUMAN_PLAYER_OPTIONS_LENGTH; continue; } } if (parts.Length <= i + 1) return; int playerOptions = Conversions.IntFromString(parts[i + 1], -1); if (playerOptions == -1) return; byte[] byteArray = new byte[sizeof(int)]; BinaryPrimitives.WriteInt32LittleEndian(byteArray, playerOptions); int team = byteArray[0]; int start = byteArray[1]; int color = byteArray[2]; int side = byteArray[3]; if (side < 0 || side > SideCount + RandomSelectorCount) return; if (color < 0 || color > MPColors.Count) return; if (start < 0 || start > MAX_PLAYER_COUNT) return; if (team < 0 || team > 4) return; pInfo.TeamId = byteArray[0]; pInfo.StartingLocation = byteArray[1]; pInfo.ColorId = byteArray[2]; pInfo.SideId = byteArray[3]; if (pInfo.IsAI) { pInfo.Ready = true; AIPlayers.Add(pInfo); i += AI_PLAYER_OPTIONS_LENGTH; } else { if (parts.Length <= i + 2) return; int readyStatus = Conversions.IntFromString(parts[i + 2], -1); if (readyStatus == -1) return; pInfo.Ready = readyStatus > 0; pInfo.AutoReady = readyStatus > 1; if (pInfo.Name == ProgramConstants.PLAYERNAME) btnLaunchGame.Text = pInfo.Ready ? BTN_LAUNCH_NOT_READY : BTN_LAUNCH_READY; Players.Add(pInfo); i += HUMAN_PLAYER_OPTIONS_LENGTH; } } CopyPlayerDataToUI(); } /// /// Broadcasts game options to non-host players /// when the host has changed an option. /// protected override void OnGameOptionChanged() { base.OnGameOptionChanged(); if (!IsHost) return; bool[] optionValues = new bool[CheckBoxes.Count]; for (int i = 0; i < CheckBoxes.Count; i++) optionValues[i] = CheckBoxes[i].Checked; // Let's pack the booleans into bytes List byteList = Conversions.BoolArrayIntoBytes(optionValues).ToList(); while (byteList.Count % 4 != 0) byteList.Add(0); int integerCount = byteList.Count / 4; byte[] byteArray = byteList.ToArray(); ExtendedStringBuilder sb = new ExtendedStringBuilder("GO ", true, ';'); for (int i = 0; i < integerCount; i++) sb.Append(BinaryPrimitives.ReadInt32LittleEndian(byteArray.AsSpan(i * 4))); // We don't gain much in most cases by packing the drop-down values // (because they're bytes to begin with, and usually non-zero), // so let's just transfer them as usual foreach (GameLobbyDropDown dd in DropDowns) sb.Append(dd.SelectedIndex); sb.Append(Convert.ToInt32(Map?.Official ?? false)); sb.Append(Map?.SHA1 ?? string.Empty); sb.Append(GameMode?.Name ?? string.Empty); sb.Append(FrameSendRate); sb.Append(MaxAhead); sb.Append(ProtocolVersion); sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map?.UntranslatedName ?? string.Empty); channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } /// /// Handles game option messages received from the game host. /// private void ApplyGameOptions(string sender, string message) { if (sender != hostName) return; string[] parts = message.Split(';'); int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; int partIndex = checkBoxIntegerCount + DropDowns.Count; if (parts.Length < partIndex + 6) { AddNotice(("The game host has sent an invalid game options message! " + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); return; } string mapOfficial = parts[partIndex]; bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); string mapSHA1 = parts[partIndex + 1]; string gameMode = parts[partIndex + 2]; int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); if (frameSendRate != FrameSendRate) { FrameSendRate = frameSendRate; AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); } int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); if (maxAhead != MaxAhead) { MaxAhead = maxAhead; AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); } int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); if (protocolVersion != ProtocolVersion) { ProtocolVersion = protocolVersion; AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); } string mapName = parts[partIndex + 8]; GameModeMap currentGameModeMap = GameModeMap; lastGameMode = gameMode; lastMapSHA1 = mapSHA1; lastMapName = mapName; GameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); if (GameModeMap == null) { ChangeMap(null); if (!string.IsNullOrEmpty(mapSHA1)) { if (!isMapOfficial) RequestMap(mapSHA1); else ShowOfficialMapMissingMessage(mapSHA1); } } else if (GameModeMap != currentGameModeMap) { ChangeMap(GameModeMap); } // By changing the game options after changing the map, we know which // game options were changed by the map and which were changed by the game host // If the map doesn't exist on the local installation, it's impossible // to know which options were set by the host and which were set by the // map, so we'll just assume that the host has set all the options. // Very few (if any) custom maps force options, so it'll be correct nearly always for (int i = 0; i < checkBoxIntegerCount; i++) { if (parts.Length <= i) return; int checkBoxStatusInt; bool success = int.TryParse(parts[i], out checkBoxStatusInt); if (!success) { AddNotice(("Failed to parse check box options sent by game host!" + "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); return; } byte[] byteArray = new byte[sizeof(int)]; BinaryPrimitives.WriteInt32LittleEndian(byteArray, checkBoxStatusInt); bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) { int gameOptionIndex = i * 32 + optionIndex; if (gameOptionIndex >= CheckBoxes.Count) break; GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; if (checkBox.Checked != boolArray[optionIndex]) { if (boolArray[optionIndex]) AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); else AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); } CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; } } for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) { if (parts.Length <= i) { AddNotice(("The game host has sent an invalid game options message! " + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); return; } int ddSelectedIndex; bool success = int.TryParse(parts[i], out ddSelectedIndex); if (!success) { AddNotice(("Failed to parse drop down options sent by game host (2)! " + "The game host's game version might be different from yours.").L10N("Client:Main:HostDropDownParseError"), Color.Red); return; } GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) continue; if (dd.SelectedIndex != ddSelectedIndex) { string ddName = dd.OptionName; if (dd.OptionName == null) ddName = dd.Name; AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); } DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; } int randomSeed; bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed); if (!parseSuccess) { AddNotice(("Failed to parse random seed from game options message! " + "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); } bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], Convert.ToInt32(RemoveStartingLocations))); SetRandomStartingLocations(removeStartingLocations); RandomSeed = randomSeed; } private void RequestMap(string mapSHA1) { if (UserINISettings.Instance.EnableMapSharing) { AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist")); mapSharingConfirmationPanel.ShowForMapDownload(); } else { AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + ("Because you've disabled map sharing, it cannot be transferred. The game host needs " + "to change the map or you will be unable to participate in the match.").L10N("Client:Main:MapSharingDisabledNotice")); channel.SendCTCPMessage(MAP_SHARING_DISABLED_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, 9); } } private void ShowOfficialMapMissingMessage(string sha1) { AddNotice(("The game host has selected an official map that doesn't exist on your installation. " + "This could mean that the game host has modified game files, or is running a different game version. " + "They need to change the map or you will be unable to participate in the match.").L10N("Client:Main:OfficialMapNotExist")); channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); } private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, EventArgs e) { Logger.Log("Map sharing confirmed."); AddNotice("Attempting to download map.".L10N("Client:Main:DownloadingMap")); mapSharingConfirmationPanel.SetDownloadingStatus(); MapSharer.DownloadMap(lastMapSHA1, localGame, lastMapName); } protected override void ChangeMap(GameModeMap gameModeMap) { mapSharingConfirmationPanel.Disable(); base.ChangeMap(gameModeMap); } protected override void HandleMapUpdated(Map updatedMap, string previousSHA1) { base.HandleMapUpdated(updatedMap, previousSHA1); // If the host's currently selected map was updated, broadcast the new map to other players if (IsHost && Map != null && Map.SHA1 == updatedMap.SHA1) OnGameOptionChanged(); } /// /// Signals other players that the local player has returned from the game, /// and unlocks the game as well as generates a new random seed as the game host. /// protected override void GameProcessExited() { ResetGameState(); } protected void GameStartAborted() { ResetGameState(); } protected void ResetGameState() { base.GameProcessExited(); channel.SendCTCPMessage("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); ReturnNotification(ProgramConstants.PLAYERNAME); if (IsHost) { RandomSeed = random.Next(); OnGameOptionChanged(); ClearReadyStatuses(); CopyPlayerDataToUI(); BroadcastPlayerOptions(); BroadcastPlayerExtraOptions(); StartInactiveCheck(); if (Players.Count < playerLimit) UnlockGame(true); } } /// /// Handles the "START" (game start) command sent by the game host. /// private void NonHostLaunchGame(string sender, string message) { if (sender != hostName) return; if (Map == null) { GameStartAborted(); return; } string[] parts = message.Split(';'); if (parts.Length < 1) return; UniqueGameID = Conversions.IntFromString(parts[0], -1); if (UniqueGameID < 0) return; var recentPlayers = new List(); for (int i = 1; i < parts.Length; i += 2) { if (parts.Length <= i + 1) return; string pName = parts[i]; string[] ipAndPort = parts[i + 1].Split(':'); if (ipAndPort.Length < 2) return; int port; bool success = int.TryParse(ipAndPort[1], out port); if (!success) return; if (pName == ProgramConstants.PLAYERNAME) { var matchedTunnel = tunnelHandler.Tunnels .FirstOrDefault(t => string.Equals(t.Address, ipAndPort[0], StringComparison.OrdinalIgnoreCase)); if (matchedTunnel != null) { tunnelHandler.CurrentTunnel = matchedTunnel; } else { XNAMessageBox.Show(WindowManager, "Tunnel Error".L10N("Client:Main:TunnelErrorTitle"), "Failed to match the tunnel address provided by the host to any available tunnel. The game cannot be started.".L10N("Client:Main:TunnelErrorMessage")); Logger.Log("Failed to match tunnel address: " + ipAndPort[0]); return; } } PlayerInfo pInfo = Players.Find(p => p.Name == pName); if (pInfo == null) return; pInfo.Port = port; recentPlayers.Add(pName); } cncnetUserData.AddRecentPlayers(recentPlayers, gameRoomName); StartGame(); } protected override void StartGame() { AddNotice("Starting game...".L10N("Client:Main:StartingGame")); FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(); if (gameFilesHash != fhc.GetCompleteHash()) { Logger.Log("Game files modified during client session!"); channel.SendCTCPMessage(CHEAT_DETECTED_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); } StopInactiveCheck(); channel.SendCTCPMessage("STRTD", QueuedMessageType.SYSTEM_MESSAGE, 20); base.StartGame(); } protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); iniFile.SetIntValue("Settings", "GameID", UniqueGameID); iniFile.SetBooleanValue("Settings", "Host", IsHost); PlayerInfo localPlayer = FindLocalPlayer(); if (localPlayer == null) return; iniFile.SetIntValue("Settings", "Port", localPlayer.Port); } protected override void SendChatMessage(string message) => channel.SendChatMessage(message, chatColor); #region Notifications private void HandleNotification(string sender, Action handler) { if (sender != hostName) return; handler(); } private void HandleIntNotification(string sender, int parameter, Action handler) { if (sender != hostName) return; handler(parameter); } protected override void GetReadyNotification() { base.GetReadyNotification(); #if WINFORMS WindowManager.FlashWindow(); #endif TopBar.SwitchToPrimary(); if (IsHost) channel.SendCTCPMessage("GETREADY", QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } protected override void AISpectatorsNotification() { base.AISpectatorsNotification(); if (IsHost) channel.SendCTCPMessage("AISPECS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void InsufficientPlayersNotification() { base.InsufficientPlayersNotification(); if (IsHost) channel.SendCTCPMessage("INSFSPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void TooManyPlayersNotification() { base.TooManyPlayersNotification(); if (IsHost) channel.SendCTCPMessage("TMPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void SharedColorsNotification() { base.SharedColorsNotification(); if (IsHost) channel.SendCTCPMessage("CLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void SharedStartingLocationNotification() { base.SharedStartingLocationNotification(); if (IsHost) channel.SendCTCPMessage("SLOC", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void LockGameNotification() { base.LockGameNotification(); if (IsHost) channel.SendCTCPMessage("LCKGME", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void NotVerifiedNotification(int playerIndex) { base.NotVerifiedNotification(playerIndex); if (IsHost) channel.SendCTCPMessage("NVRFY " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override void StillInGameNotification(int playerIndex) { base.StillInGameNotification(playerIndex); if (IsHost) channel.SendCTCPMessage("INGM " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } private void GameStartedNotification(string sender) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo != null) pInfo.IsInGame = true; CopyPlayerDataToUI(); } private void ReturnNotification(string sender) { AddNotice(string.Format("{0} has returned from the game.".L10N("Client:Main:PlayerReturned"), sender)); PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo != null) pInfo.IsInGame = false; sndReturnSound.Play(); CopyPlayerDataToUI(); } private void HandleTunnelPing(string sender, int ping) { PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender)); if (pInfo != null) { pInfo.Ping = ping; UpdatePlayerPingIndicator(pInfo); } } private void FileHashNotification(string sender, string filesHash) { if (!IsHost) return; PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo != null) pInfo.HashReceived = true; CopyPlayerDataToUI(); if (filesHash != gameFilesHash) { channel.SendCTCPMessage("MM " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); CheaterNotification(ProgramConstants.PLAYERNAME, sender); } } private void CheaterNotification(string sender, string cheaterName) { if (sender != hostName) return; AddNotice(string.Format("Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); } protected override void BroadcastDiceRoll(int dieSides, int[] results) { string resultString = string.Join(",", results); channel.SendCTCPMessage($"{DICE_ROLL_MESSAGE} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); } #endregion protected override void HandleLockGameButtonClick() { if (!Locked) { AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); LockGame(); } else { if (Players.Count < playerLimit) { AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnlockedByYou")); UnlockGame(false); } else AddNotice(string.Format( "Cannot unlock game; the player limit ({0}) has been reached.".L10N("Client:Main:RoomCantUnlockAsLimit"), playerLimit)); } } protected override void LockGame() { connectionManager.SendCustomMessage(new QueuedMessage( string.Format("MODE {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = true; btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); AccelerateGameBroadcasting(); } protected override void UnlockGame(bool announce) { connectionManager.SendCustomMessage(new QueuedMessage( string.Format("MODE {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = false; if (announce) AddNotice("The game room has been unlocked.".L10N("Client:Main:GameRoomUnlocked")); btnLockGame.Text = "Lock Game".L10N("Client:Main:LockGame"); AccelerateGameBroadcasting(); } protected override void KickPlayer(int playerIndex) { if (playerIndex >= Players.Count) return; var pInfo = Players[playerIndex]; AddNotice(string.Format("Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); channel.SendKickMessage(pInfo.Name, 8); } protected override void BanPlayer(int playerIndex) { if (playerIndex >= Players.Count) return; var pInfo = Players[playerIndex]; var user = connectionManager.UserList.Find(u => u.Name == pInfo.Name); if (user != null) { AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); channel.SendBanMessage(user.Hostname, 8); channel.SendKickMessage(user.Name, 8); } } private void HandleCheatDetectedMessage(string sender) => AddNotice(string.Format("{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort) { if (sender != hostName) return; string[] split = tunnelAddressAndPort.Split(':'); string tunnelAddress = split[0]; int tunnelPort = int.Parse(split[1]); CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); if (tunnel == null) { tunnelErrorMode = true; AddNotice(("The game host has selected an invalid tunnel server! " + "The game host needs to change the server or you will be unable " + "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), Color.Yellow); UpdateLaunchGameButtonStatus(); return; } tunnelErrorMode = false; HandleTunnelServerChange(tunnel); UpdateLaunchGameButtonStatus(); } /// /// Changes the tunnel server used for the game. /// /// The new tunnel server to use. private void HandleTunnelServerChange(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); foreach (PlayerInfo pInfo in Players) { pInfo.Ping = -1; UpdatePlayerPingIndicator(pInfo); } CopyPlayerDataToUI(); UpdatePing(); } protected override bool UpdateLaunchGameButtonStatus() { btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && !tunnelErrorMode; return btnLaunchGame.Enabled; } #region CnCNet map sharing private void MapSharer_MapDownloadFailed(object sender, SHA1EventArgs e) => WindowManager.AddCallback(new Action(MapSharer_HandleMapDownloadFailed), e); private void MapSharer_HandleMapDownloadFailed(SHA1EventArgs e) { // If the host has already uploaded the map, we shouldn't request them to re-upload it if (hostUploadedMaps.Contains(e.SHA1)) { AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); mapSharingConfirmationPanel.SetFailedStatus(); channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); return; } else if (chatCommandDownloadedMaps.Contains(e.SHA1)) { // Notify the user that their chat command map download failed. // Do not notify other users with a CTCP message as this is irrelevant to them. AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); mapSharingConfirmationPanel.SetFailedStatus(); return; } AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); channel.SendCTCPMessage(MAP_SHARING_UPLOAD_REQUEST + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } private void MapSharer_MapDownloadComplete(object sender, SHA1EventArgs e) => WindowManager.AddCallback(new Action(MapSharer_HandleMapDownloadComplete), e); private void MapSharer_HandleMapDownloadComplete(SHA1EventArgs e) { string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); Logger.Log("Map " + mapFileName + " downloaded successfully."); // MapLoader_MapChanged will fire when it's processed. } private void MapLoader_MapChanged(object sender, MapChangedEventArgs e) { if (e.ChangeType != MapChangeType.Added) return; bool isFromChatCommand = chatCommandDownloadedMaps.Contains(e.Map.SHA1); bool isFromHostSharing = lastMapSHA1 == e.Map.SHA1 && !isFromChatCommand; if (!isFromChatCommand && !isFromHostSharing) return; AddNotice($"Map {e.Map.Name} loaded successfully."); GameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.Map.SHA1 == e.Map.SHA1); ChangeMap(GameModeMap); if (isFromChatCommand) chatCommandDownloadedMaps.Remove(e.Map.SHA1); } protected override void HandleMapAdded(Map addedMap) { bool isFromChatCommand = chatCommandDownloadedMaps.Contains(addedMap.SHA1); bool isFromHostSharing = lastMapSHA1 == addedMap.SHA1 && !isFromChatCommand; // If this is a map we downloaded, select it if (isFromChatCommand || isFromHostSharing) { AddNotice($"Map {addedMap.Name} loaded successfully."); RefreshGameModeFilter(); GameModeMap gameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.Map.SHA1 == addedMap.SHA1); if (gameModeMap != null) { // select game mode int gameModeIndex = ddGameModeMapFilter.Items.FindIndex(item => (item.Tag as GameModeMapFilter)?.GetGameModeMaps().Any(gmm => gmm.GameMode.Name == gameModeMap.GameMode.Name) ?? false); if (gameModeIndex >= 0) ddGameModeMapFilter.SelectedIndex = gameModeIndex; ListMaps(); // select map for (int i = 0; i < lbGameModeMapList.ItemCount; i++) { var item = lbGameModeMapList.GetItem(1, i); if ((item.Tag as GameModeMap)?.Map.SHA1 == addedMap.SHA1) { lbGameModeMapList.SelectedIndex = i; break; } } ChangeMap(gameModeMap); } if (isFromChatCommand) chatCommandDownloadedMaps.Remove(addedMap.SHA1); } else { base.HandleMapAdded(addedMap); } } private void MapSharer_MapUploadFailed(object sender, MapEventArgs e) => WindowManager.AddCallback(new Action(MapSharer_HandleMapUploadFailed), e); private void MapSharer_HandleMapUploadFailed(MapEventArgs e) { Map map = e.Map; hostUploadedMaps.Add(map.SHA1); AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); if (map == Map) { AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } private void MapSharer_MapUploadComplete(object sender, MapEventArgs e) => WindowManager.AddCallback(new Action(MapSharer_HandleMapUploadComplete), e); private void MapSharer_HandleMapUploadComplete(MapEventArgs e) { hostUploadedMaps.Add(e.Map.SHA1); AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); if (e.Map == Map) { channel.SendCTCPMessage(MAP_SHARING_DOWNLOAD_REQUEST + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } /// /// Handles a map upload request sent by a player. /// /// The sender of the request. /// The SHA1 of the requested map. private void HandleMapUploadRequest(string sender, string mapSHA1) { if (hostUploadedMaps.Contains(mapSHA1)) { Logger.Log("HandleMapUploadRequest: Map " + mapSHA1 + " is already uploaded!"); return; } Map map = null; foreach (GameMode gm in GameModeMaps.GameModes) { map = gm.Maps.Find(m => m.SHA1 == mapSHA1); if (map != null) break; } if (map == null) { Logger.Log("Unknown map upload request from " + sender + ": " + mapSHA1); return; } if (map.Official) { Logger.Log("HandleMapUploadRequest: Map is official, so skip request"); AddNotice(string.Format(("{0} doesn't have the map '{1}' on their local installation. " + "The map needs to be changed or {0} is unable to participate in the match.").L10N("Client:Main:PlayerMissingMap"), sender, map.Name)); return; } if (!IsHost) return; AddNotice(string.Format(("{0} doesn't have the map '{1}' on their local installation. " + "Attempting to upload the map to the CnCNet map database.").L10N("Client:Main:UpdateMapToDBPrompt"), sender, map.Name)); MapSharer.UploadMap(map, localGame); } /// /// Handles a map transfer failure message sent by either the player or the game host. /// private void HandleMapTransferFailMessage(string sender, string sha1) { if (sender == hostName) { AddNotice("The game host failed to upload the map to the CnCNet map database.".L10N("Client:Main:HostUpdateMapToDBFailed")); hostUploadedMaps.Add(sha1); if (lastMapSHA1 == sha1 && Map == null) { AddNotice("The game host needs to change the map or you won't be able to participate in this match.".L10N("Client:Main:HostMustChangeMap")); } return; } if (lastMapSHA1 == sha1) { if (!IsHost) { AddNotice(string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + "The host needs to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:HostNeedChangeMapForPlayer"), sender)); } else { AddNotice(string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + "You need to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:YouNeedChangeMapForPlayer"), sender)); } } } private void HandleMapDownloadRequest(string sender, string sha1) { if (sender != hostName) return; hostUploadedMaps.Add(sha1); if (lastMapSHA1 == sha1 && Map == null) { Logger.Log("The game host has uploaded the map into the database. Re-attempting download..."); MapSharer.DownloadMap(sha1, localGame, lastMapName); } } private void HandleMapSharingBlockedMessage(string sender) { AddNotice(string.Format(("The selected map doesn't exist on {0}'s installation, and they " + "have map sharing disabled in settings. The game host needs to change to a non-custom map or " + "they will be unable to participate in this match.").L10N("Client:Main:PlayerMissingMapDisabledSharing"), sender)); } /// /// Download a map from CNCNet using a map hash ID. /// /// Users and testers can get map hash IDs from this URL template: /// /// - http://mapdb.cncnet.org/search.php?game=GAME_ID&search=MAP_NAME_SEARCH_STRING /// /// /// /// This is a string beginning with the sha1 hash map ID, and (optionally) the name to use as a local filename for the map file. /// Every character after the first space will be treated as part of the map name. /// /// "?" characters are removed from the sha1 due to weird copy and paste behavior from the map search endpoint. /// private void DownloadMapByIdCommand(string parameters) { string sha1; string mapName; string message; // Make sure no spaces at the beginning or end of the string will mess up arg parsing. parameters = parameters.Trim(); // Check if the parameter's contain spaces. // The presence of spaces indicates a user-specified map name. int firstSpaceIndex = parameters.IndexOf(' '); if (firstSpaceIndex == -1) { // The user did not supply a map name. sha1 = parameters; mapName = "user_chat_command_download"; } else { // User supplied a map name. sha1 = parameters.Substring(0, firstSpaceIndex); mapName = parameters.Substring(firstSpaceIndex + 1); mapName = mapName.Trim(); } // Remove erroneous "?". These sneak in when someone double-clicks a map ID and copies it from the cncnet search endpoint. // There is some weird whitespace that gets copied to chat as a "?" at the end of the hash. It's hard to spot, so just hold the user's hand. sha1 = sha1.Replace("?", ""); // See if the user already has this map, with any filename, before attempting to download it. GameModeMap loadedMap = GameModeMaps.FirstOrDefault(gmm => gmm.Map.SHA1 == sha1); if (loadedMap != null) { message = String.Format( "The map for ID \"{0}\" is already loaded from \"{1}.{2}\", delete the existing file before trying again.".L10N("Client:Main:DownloadMapCommandSha1AlreadyExists"), sha1, loadedMap.Map.BaseFilePath, ClientConfiguration.Instance.MapFileExtension); AddNotice(message, Color.Yellow); Logger.Log(message); return; } // Replace any characters that are not safe for filenames. char replaceUnsafeCharactersWith = '-'; // Use a hashset instead of an array for quick lookups in `invalidChars.Contains()`. HashSet invalidChars = new HashSet(Path.GetInvalidFileNameChars()); string safeMapName = new String(mapName.Select(c => invalidChars.Contains(c) ? replaceUnsafeCharactersWith : c).ToArray()); chatCommandDownloadedMaps.Add(sha1); message = String.Format("Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("Client:Main:DownloadMapCommandStartingDownload"), sha1, mapName); Logger.Log(message); AddNotice(message); MapSharer.DownloadMap(sha1, localGame, safeMapName); } #endregion #region Game broadcasting logic /// /// Lowers the time until the next game broadcasting message. /// private void AccelerateGameBroadcasting() => gameBroadcastTimer.Accelerate(TimeSpan.FromSeconds(GAME_BROADCAST_ACCELERATION)); private void BroadcastGame() { Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); if (broadcastChannel == null) return; if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) return; StringBuilder sb = new StringBuilder("GAME "); sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); sb.Append(";"); sb.Append(ProgramConstants.GAME_VERSION); sb.Append(";"); sb.Append(playerLimit); sb.Append(";"); sb.Append(channel.ChannelName); sb.Append(";"); sb.Append(gameRoomName); sb.Append(";"); if (Locked) sb.Append("1"); else sb.Append("0"); sb.Append(Convert.ToInt32(isCustomPassword)); sb.Append(Convert.ToInt32(closed)); sb.Append("0"); // IsLoadedGame sb.Append("0"); // IsLadder sb.Append(";"); foreach (PlayerInfo pInfo in Players) { sb.Append(pInfo.Name); sb.Append(","); } sb.Remove(sb.Length - 1, 1); sb.Append(";"); sb.Append(Map?.UntranslatedName ?? string.Empty); sb.Append(";"); sb.Append(GameMode?.UntranslatedUIName ?? string.Empty); sb.Append(";"); sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); sb.Append(";"); sb.Append(0); // LoadedGameId sb.Append(";"); sb.Append(skillLevel); // SkillLevel sb.Append(";"); sb.Append(Map?.SHA1); List broadcastableSettings = GetBroadcastableSettings(); List gameOptionValues = new(); int checkboxCount = CheckBoxes.Count(cb => cb.BroadcastToLobby); if (checkboxCount > 0) { bool[] checkboxValues = new bool[checkboxCount]; for (int i = 0; i < checkboxCount; i++) checkboxValues[i] = CheckBoxes.Where(cb => cb.BroadcastToLobby).ElementAt(i).Checked; List byteList = Conversions.BoolArrayIntoBytes(checkboxValues).ToList(); // Pad to multiple of 4 bytes while (byteList.Count % 4 != 0) byteList.Add(0); byte[] byteArray = byteList.ToArray(); // Convert bytes to integers for (int i = 0; i < byteArray.Length / 4; i++) gameOptionValues.Add(BinaryPrimitives.ReadInt32LittleEndian(byteArray.AsSpan(i * 4))); } // Add dropdown indices int dropdownCount = DropDowns.Count(dd => dd.BroadcastToLobby); if (dropdownCount > 0) gameOptionValues.AddRange(DropDowns.Where(dd => dd.BroadcastToLobby).Select(dd => dd.SelectedIndex)); sb.Append(";"); if (gameOptionValues.Count > 0) sb.Append(string.Join(",", gameOptionValues)); broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } #endregion public override string GetSwitchName() => "Game Lobby".L10N("Client:Main:GameLobby"); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/CommandHandlerBase.cs ================================================ namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers { public abstract class CommandHandlerBase { public CommandHandlerBase(string commandName) { CommandName = commandName; } public string CommandName { get; private set; } public abstract bool Handle(string sender, string message); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntCommandHandler.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers { public class IntCommandHandler : CommandHandlerBase { public IntCommandHandler(string commandName, Action handler) : base(commandName) { this.handler = handler; } Action handler; public override bool Handle(string sender, string message) { if (message.Length < CommandName.Length + 1) return false; if (message.StartsWith(CommandName)) { int value; bool success = int.TryParse(message.Substring(CommandName.Length + 1), out value); if (success) { handler(sender, value); return true; } } return false; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntNotificationHandler.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers { public class IntNotificationHandler : CommandHandlerBase { public IntNotificationHandler(string commandName, Action> action, Action innerAction) : base(commandName) { this.action = action; this.innerAction = innerAction; } Action> action; Action innerAction; public override bool Handle(string sender, string message) { if (message.StartsWith(CommandName)) { string intPart = message.Substring(CommandName.Length + 1); int value; bool success = int.TryParse(intPart, out value); action(sender, value, innerAction); return true; } return false; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/NoParamCommandHandler.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers { /// /// A command handler that handles a command that has no parameter aside from the sender. /// public class NoParamCommandHandler : CommandHandlerBase { public NoParamCommandHandler(string commandName, Action commandHandler) : base(commandName) { this.commandHandler = commandHandler; } Action commandHandler; public override bool Handle(string sender, string message) { if (message == CommandName) { commandHandler(sender); return true; } return false; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/NotificationHandler.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers { public class NotificationHandler : CommandHandlerBase { public NotificationHandler(string commandName, Action action, Action innerAction) : base(commandName) { this.action = action; this.innerAction = innerAction; } Action action; Action innerAction; public override bool Handle(string sender, string message) { if (message == CommandName) { action(sender, innerAction); return true; } return false; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers { class StringCommandHandler : CommandHandlerBase { public StringCommandHandler(string commandName, Action commandHandler) : base(commandName) { this.commandHandler = commandHandler; } private Action commandHandler; public override bool Handle(string sender, string message) { if (message.Length < CommandName.Length + 1) return false; if (message.StartsWith(CommandName)) { string parameters = message.Substring(CommandName.Length + 1); commandHandler.Invoke(sender, parameters); //commandHandler(sender, message.Substring(CommandName.Length + 1)); return true; } return false; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/CoopBriefingBox.cs ================================================ using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; using Microsoft.Xna.Framework; using Rampastring.Tools; namespace DTAClient.DXGUI.Multiplayer.GameLobby { /// /// A box for drawing scenario briefings. /// class CoopBriefingBox : XNAPanel { private const int MARGIN = 12; private const float ALPHA_RATE = 0.4f; public CoopBriefingBox(WindowManager windowManager) : base(windowManager) { } /// /// The index of the text font. /// public int FontIndex { get; set; } = 0; string text = string.Empty; private bool isVisible = true; public override void Initialize() { DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; ClientRectangle = new Rectangle(0, 0, 400, 300); PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 224), 2, 2); InputEnabled = false; AlphaRate = ALPHA_RATE; base.Initialize(); CenterOnParent(); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "FontIndex": FontIndex = Conversions.IntFromString(value, 0); return; } base.ParseControlINIAttribute(iniFile, key, value); } public void SetFadeVisibility(bool visible) { isVisible = visible; } public void SetAlpha(float alpha) { Alpha = alpha; } public void SetText(string text) { this.text = Renderer.FixText(text, FontIndex, Width - (MARGIN * 2)).Text; int textHeight = (int)Renderer.GetTextDimensions(this.text, FontIndex).Y; ClientRectangle = new Rectangle(X, 0, Width, textHeight + MARGIN * 2); CenterOnParent(); } public override void Update(GameTime gameTime) { if (isVisible) { AlphaRate = ALPHA_RATE; } else { AlphaRate = -ALPHA_RATE; } base.Update(gameTime); } public override void Draw(GameTime gameTime) { //base.Draw(gameTime); FillControlArea(new Color(0, 0, 0, 224)); DrawRectangle(new Rectangle(0, 0, Width, Height), BorderColor); DrawStringWithShadow(text, FontIndex, new Vector2(MARGIN, MARGIN), UISettings.ActiveSettings.AltColor); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameHostInactiveChecker.cs ================================================ using System; using System.Timers; using ClientCore; using ClientCore.Extensions; using ClientGUI; using Rampastring.XNAUI; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class GameHostInactiveChecker { private readonly WindowManager windowManager; private readonly Timer timer; private bool isWarningShown; private DateTime startTime; private static int WarningSeconds => ClientConfiguration.Instance.InactiveHostWarningMessageSeconds; private static int CloseSeconds => ClientConfiguration.Instance.InactiveHostKickSeconds; public event EventHandler CloseEvent; public GameHostInactiveChecker(WindowManager windowManager) { this.windowManager = windowManager; timer = new Timer(); timer.AutoReset = true; timer.Interval = 1000; timer.Elapsed += TimerOnElapsed; } private void TimerOnElapsed(object sender, ElapsedEventArgs e) { double secondsElapsed = (DateTime.UtcNow - startTime).TotalSeconds; if (secondsElapsed > WarningSeconds && !isWarningShown) ShowWarning(); if (secondsElapsed > CloseSeconds) SendCloseEvent(); } public void Start() { Reset(); timer.Start(); } public void Reset() { startTime = DateTime.UtcNow; isWarningShown = false; } public void Stop() => timer.Stop(); private void SendCloseEvent() { Stop(); CloseEvent?.Invoke(this, null); } private void ShowWarning() { isWarningShown = true; XNAMessageBox hostInactiveWarningMessageBox = new XNAMessageBox( windowManager, "Are you still here?".L10N("Client:Main:InactiveHostWarningTitle"), "Your game may be closed due to inactivity.".L10N("Client:Main:InactiveHostWarningText"), XNAMessageBoxButtons.OK ); hostInactiveWarningMessageBox.OKClickedAction = box => Reset(); hostInactiveWarningMessageBox.Show(); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLaunchButton.cs ================================================ using ClientGUI; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class GameLaunchButton : XNAClientButton { public GameLaunchButton(WindowManager windowManager) : base(windowManager) { } private StarDisplay starDisplay; public void InitStarDisplay(Texture2D[] rankTextures) { if (starDisplay != null) throw new InvalidOperationException("The star display is already initialized!"); starDisplay = new StarDisplay(WindowManager, rankTextures); starDisplay.InputEnabled = false; AddChild(starDisplay); ClientRectangleUpdated += (e, sender) => UpdateStarPosition(); UpdateStarPosition(); } public override void Initialize() { base.Initialize(); } public override string Text { get => base.Text; set { base.Text = value; UpdateStarPosition(); } } private void UpdateStarPosition() { if (starDisplay == null) return; starDisplay.Y = (Height - starDisplay.Height) / 2; starDisplay.X = (Width / 2) + (int)(Renderer.GetTextDimensions(Text, FontIndex).X / 2) + 3; } public void SetRank(int rank) { starDisplay.Rank = rank; UpdateStarPosition(); } } class StarDisplay : XNAControl { public StarDisplay(WindowManager windowManager, Texture2D[] rankTextures) : base(windowManager) { Name = "StarDisplay"; this.rankTextures = rankTextures; Width = rankTextures[1].Width; Height = rankTextures[1].Height; } private readonly Texture2D[] rankTextures; public int Rank { get; set; } public override void Initialize() { base.Initialize(); } public override void Draw(GameTime gameTime) { DrawTexture(rankTextures[Rank], Point.Zero, Color.White); base.Draw(gameTime); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLeftEventArgs.cs ================================================ #nullable enable namespace DTAClient.DXGUI.Multiplayer.GameLobby; public class GameLeftEventArgs { public string? Message { get; init; } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs ================================================ using ClientCore; using ClientCore.Statistics; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.Multiplayer; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using System.Linq; using ClientCore.Enums; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; using ClientCore.Extensions; using DTAClient.DXGUI.Generic; using TextCopy; using System.Diagnostics; namespace DTAClient.DXGUI.Multiplayer.GameLobby { /// /// A generic base for all game lobbies (Skirmish, LAN and CnCNet). /// Contains the common logic for parsing game options and handling player info. /// public abstract class GameLobbyBase : INItializableWindow { protected record Rank { private readonly int rank; public static readonly Rank None = 0; public static readonly Rank Easy = 1; public static readonly Rank Medium = 2; public static readonly Rank Hard = 3; private Rank(int rank) => this.rank = rank; public static implicit operator int(Rank value) => value.rank; public static implicit operator Rank(int value) => new Rank(value); } protected const int MAX_PLAYER_COUNT = 8; protected const int PLAYER_OPTION_VERTICAL_MARGIN = 12; protected const int PLAYER_OPTION_HORIZONTAL_MARGIN = 3; protected const int PLAYER_OPTION_CAPTION_Y = 6; private const int DROP_DOWN_HEIGHT = 21; protected readonly string BTN_LAUNCH_GAME = "Launch Game".L10N("Client:Main:ButtonLaunchGame"); protected readonly string BTN_LAUNCH_READY = "I'm Ready".L10N("Client:Main:ButtonIAmReady"); protected readonly string BTN_LAUNCH_NOT_READY = "Not Ready".L10N("Client:Main:ButtonNotReady"); private readonly string FavoriteMapsLabel = "Favorites".L10N("Client:Main:Favorites"); /// /// Creates a new instance of the game lobby base. /// /// /// The name of the lobby in GameOptions.ini. /// /// /// public GameLobbyBase( WindowManager windowManager, string iniName, MapLoader mapLoader, bool isMultiplayer, DiscordHandler discordHandler, Random random ) : base(windowManager) { _iniSectionName = iniName; MapLoader = mapLoader; this.isMultiplayer = isMultiplayer; this.discordHandler = discordHandler; this.random = random; } private string _iniSectionName; private Random random; protected XNAPanel PlayerOptionsPanel; protected List MPColors; public List CheckBoxes { get; } = new(); public List DropDowns { get; } = new(); public List GetBroadcastableSettings() { var result = new List(); result.AddRange(CheckBoxes.Where(cb => cb.BroadcastToLobby)); result.AddRange(DropDowns.Where(dd => dd.BroadcastToLobby)); return result; } protected DiscordHandler discordHandler; protected MapLoader MapLoader; /// /// The list of multiplayer game mode maps. /// Each is an instance of a map for a specific game mode. /// protected IReadOnlyGameModeMapCollection GameModeMaps => MapLoader.GameModeMaps; protected GameModeMapFilter gameModeMapFilter; private GameModeMap _gameModeMap; /// /// The currently selected game mode. /// protected GameModeMap GameModeMap { get => _gameModeMap; set { var oldGameModeMap = _gameModeMap; _gameModeMap = value; if (value != null && oldGameModeMap != value) UpdateDiscordPresence(); } } protected Map Map => GameModeMap?.Map; protected GameMode GameMode => GameModeMap?.GameMode; protected XNAClientDropDown[] ddPlayerNames; protected XNAClientDropDown[] ddPlayerSides; protected XNAClientColorDropDown[] ddPlayerColors; protected XNAClientDropDown[] ddPlayerStarts; protected XNAClientDropDown[] ddPlayerTeams; protected XNAClientButton btnPlayerExtraOptionsOpen; protected PlayerExtraOptionsPanel PlayerExtraOptionsPanel; protected XNAClientButton btnLeaveGame; protected GameLaunchButton btnLaunchGame; protected XNAClientButton btnPickRandomMap; protected XNALabel lblMapName; protected XNALabel lblMapAuthor; protected XNALabel lblGameMode; protected XNALabel lblMapSize; protected MapPreviewBox MapPreviewBox; protected XNAMultiColumnListBox lbGameModeMapList; protected ToolTip mapListTooltip; protected XNAClientDropDown ddGameModeMapFilter; protected XNALabel lblGameModeSelect; protected XNAContextMenu mapContextMenu; private XNAContextMenuItem toggleFavoriteItem; protected XNAClientStateButton btnMapSortAlphabetically; protected XNASuggestionTextBox tbMapSearch; protected XNAContextMenu searchContextMenu; private XNAContextMenuItem searchCurrentModeItem; private XNAContextMenuItem searchAllModesItem; private bool searchAllGameModes = false; protected List Players = new List(); protected List AIPlayers = new List(); protected virtual PlayerInfo FindLocalPlayer() => Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); protected bool PlayerUpdatingInProgress { get; set; } protected Texture2D[] RankTextures; /// /// The seed used for randomizing player options. /// protected int RandomSeed { get; set; } /// /// An unique identifier for this game. /// protected int UniqueGameID { get; set; } protected int SideCount { get; private set; } protected int RandomSelectorCount { get; private set; } = 1; /// /// The maximum number of players allowed in this lobby. /// protected virtual int MaxPlayerCount => MAX_PLAYER_COUNT; protected List RandomSelectors = new List(); private readonly bool isMultiplayer = false; private MatchStatistics matchStatistics; private bool disableGameOptionUpdateBroadcast = false; protected EventHandler MultiplayerNameRightClicked; /// /// If set, the client will remove all starting waypoints from the map /// before launching it. /// protected bool RemoveStartingLocations { get; set; } = false; protected IniFile GameOptionsIni { get; private set; } protected XNAClientButton btnSaveLoadGameOptions { get; set; } private XNAContextMenu loadSaveGameOptionsMenu { get; set; } private LoadOrSaveGameOptionPresetWindow loadOrSaveGameOptionPresetWindow; public override void Initialize() { Name = _iniSectionName; //if (WindowManager.RenderResolutionY < 800) // ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY); //else ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 60, WindowManager.RenderResolutionY - 32); WindowManager.CenterControlOnScreen(this); BackgroundTexture = AssetLoader.LoadTexture("gamelobbybg.png"); RankTextures = new Texture2D[4] { AssetLoader.LoadTexture("rankNone.png"), AssetLoader.LoadTexture("rankEasy.png"), AssetLoader.LoadTexture("rankNormal.png"), AssetLoader.LoadTexture("rankHard.png") }; MPColors = MultiplayerColor.LoadColors(); GameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ClientConfiguration.GAME_OPTIONS)); base.Initialize(); try { PlayerOptionsPanel = FindChild(nameof(PlayerOptionsPanel)); } catch (Exception ex) { throw new Exception(string.Format(("It seems the client configuration was not migrated to accommodate " + "for the 'Tiberian Sun Client v6 Changes'.\n\n" + "Please refer to documentation of the client {0} for more details. This link can also be found in the log file.\n\n" + "Error message: {1}").L10N("Client:Main:NotMigratedClientException"), "https://github.com/CnCNet/xna-cncnet-client/", ex.Message)); } btnLeaveGame = FindChild(nameof(btnLeaveGame)); btnLeaveGame.LeftClick += BtnLeaveGame_LeftClick; btnLaunchGame = FindChild(nameof(btnLaunchGame)); btnLaunchGame.LeftClick += BtnLaunchGame_LeftClick; btnLaunchGame.InitStarDisplay(RankTextures); MapPreviewBox = FindChild("MapPreviewBox"); MapPreviewBox.SetFields(Players, AIPlayers, MPColors, GameOptionsIni.GetStringValue("General", "Sides", String.Empty).Split(','), GameOptionsIni); MapPreviewBox.ToggleFavorite += MapPreviewBox_ToggleFavorite; lblMapName = FindChild(nameof(lblMapName)); lblMapAuthor = FindChild(nameof(lblMapAuthor)); lblGameMode = FindChild(nameof(lblGameMode)); lblMapSize = FindChild(nameof(lblMapSize)); lbGameModeMapList = FindChild("lbMapList"); // lbMapList for backwards compatibility lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged; lbGameModeMapList.RightClick += LbGameModeMapList_RightClick; lbGameModeMapList.AllowKeyboardInput = true; //!isMultiplayer mapListTooltip = new(WindowManager, masterControl: lbGameModeMapList); mapListTooltip.FollowCursor = true; lbGameModeMapList.HoveredIndexChanged += LbGameModeMapList_HoveredIndexChanged; mapContextMenu = new XNAContextMenu(WindowManager); mapContextMenu.Name = nameof(mapContextMenu); mapContextMenu.Width = 192; // TODO autosizing mapContextMenu.AddItem("Favorite".L10N("Client:Main:Favorite"), selectAction: ToggleFavoriteMap); toggleFavoriteItem = mapContextMenu.Items.First(); mapContextMenu.AddItem("Copy Map Name".L10N("Client:Main:CopyMapName"), selectAction: () => ClipboardService.SetText(Map?.Name)); mapContextMenu.AddItem("Copy Original Name".L10N("Client:Main:CopyOriginalMapName"), selectAction: () => ClipboardService.SetText(Map?.UntranslatedName), visibilityChecker: () => Map?.UntranslatedName != Map?.Name); mapContextMenu.AddItem("Delete Map".L10N("Client:Main:DeleteMap"), selectAction: DeleteMapConfirmation, visibilityChecker: CanDeleteMap); mapContextMenu.AddItem("Show in folder".L10N("Client:Main:ShowInFolder"), selectAction: ShowInFolder); AddChild(mapContextMenu); XNAPanel rankHeader = new XNAPanel(WindowManager); rankHeader.BackgroundTexture = AssetLoader.LoadTexture("rank.png"); rankHeader.ClientRectangle = new Rectangle(0, 0, rankHeader.BackgroundTexture.Width, 19); XNAListBox rankListBox = new XNAListBox(WindowManager); rankListBox.TextBorderDistance = 2; lbGameModeMapList.AddColumn(rankHeader, rankListBox); lbGameModeMapList.AddColumn("MAP NAME".L10N("Client:Main:MapNameHeader"), lbGameModeMapList.Width - RankTextures[1].Width - 3); ddGameModeMapFilter = FindChild("ddGameMode"); // ddGameMode for backwards compatibility ddGameModeMapFilter.SelectedIndexChanged += DdGameModeMapFilter_SelectedIndexChanged; ddGameModeMapFilter.AddItem(CreateGameFilterItem(FavoriteMapsLabel, new GameModeMapFilter(GetFavoriteGameModeMaps))); foreach (GameMode gm in GameModeMaps.GameModes) ddGameModeMapFilter.AddItem(CreateGameFilterItem(gm.UIName, new GameModeMapFilter(GetGameModeMaps(gm)))); lblGameModeSelect = FindChild(nameof(lblGameModeSelect)); InitBtnMapSort(); tbMapSearch = FindChild(nameof(tbMapSearch)); tbMapSearch.InputReceived += TbMapSearch_InputReceived; tbMapSearch.RightClick += TbMapSearch_RightClick; searchContextMenu = new XNAContextMenu(WindowManager); searchContextMenu.Name = nameof(searchContextMenu); searchContextMenu.Width = 150; searchCurrentModeItem = new XNAContextMenuItem() { Text = "Search current mode".L10N("Client:Main:SearchCurrentMode"), SelectAction = () => SetSearchAllGameModes(false), HintTextGenerator = () => !searchAllGameModes ? "< " : null }; searchContextMenu.AddItem(searchCurrentModeItem); searchAllModesItem = new XNAContextMenuItem() { Text = "Search all modes".L10N("Client:Main:SearchAllModes"), SelectAction = () => SetSearchAllGameModes(true), HintTextGenerator = () => searchAllGameModes ? "< " : null }; searchContextMenu.AddItem(searchAllModesItem); searchAllGameModes = UserINISettings.Instance.SearchAllGameModes.Value; AddChild(searchContextMenu); btnPickRandomMap = FindChild(nameof(btnPickRandomMap)); btnPickRandomMap.LeftClick += BtnPickRandomMap_LeftClick; CheckBoxes.ForEach(chk => chk.CheckedChanged += ChkBox_CheckedChanged); DropDowns.ForEach(dd => dd.SelectedIndexChanged += Dropdown_SelectedIndexChanged); InitializeGameOptionPresetUI(); } /// /// Until the GUICreator can handle typed classes, this must remain manually done. /// private void InitBtnMapSort() { btnMapSortAlphabetically = new XNAClientStateButton(WindowManager, new Dictionary() { { SortDirection.None, AssetLoader.LoadTexture("sortAlphaNone.png") }, { SortDirection.Asc, AssetLoader.LoadTexture("sortAlphaAsc.png") }, { SortDirection.Desc, AssetLoader.LoadTexture("sortAlphaDesc.png") }, }); btnMapSortAlphabetically.Name = nameof(btnMapSortAlphabetically); btnMapSortAlphabetically.ClientRectangle = new Rectangle( ddGameModeMapFilter.X + -ddGameModeMapFilter.Height - 4, ddGameModeMapFilter.Y, ddGameModeMapFilter.Height, ddGameModeMapFilter.Height ); btnMapSortAlphabetically.LeftClick += BtnMapSortAlphabetically_LeftClick; btnMapSortAlphabetically.SetToolTipText("Sort Maps Alphabetically".L10N("Client:Main:MapSortAlphabeticallyToolTip")); RefreshMapSortAlphabeticallyBtn(); AddChild(btnMapSortAlphabetically); // Allow repositioning / disabling in INI. ReadINIForControl(btnMapSortAlphabetically); MapLoader.MapChanged += MapLoader_MapChanged; } private void InitializeGameOptionPresetUI() { btnSaveLoadGameOptions = FindChild(nameof(btnSaveLoadGameOptions), true); if (btnSaveLoadGameOptions != null) { loadOrSaveGameOptionPresetWindow = new LoadOrSaveGameOptionPresetWindow(WindowManager); loadOrSaveGameOptionPresetWindow.Name = nameof(loadOrSaveGameOptionPresetWindow); loadOrSaveGameOptionPresetWindow.PresetLoaded += (sender, s) => HandleGameOptionPresetLoadCommand(s); loadOrSaveGameOptionPresetWindow.PresetSaved += (sender, s) => HandleGameOptionPresetSaveCommand(s); loadOrSaveGameOptionPresetWindow.Disable(); var loadConfigMenuItem = new XNAContextMenuItem() { Text = "Load".L10N("Client:Main:ButtonLoad"), SelectAction = () => loadOrSaveGameOptionPresetWindow.Show(true) }; var saveConfigMenuItem = new XNAContextMenuItem() { Text = "Save".L10N("Client:Main:ButtonSave"), SelectAction = () => loadOrSaveGameOptionPresetWindow.Show(false) }; loadSaveGameOptionsMenu = new XNAContextMenu(WindowManager); loadSaveGameOptionsMenu.Name = nameof(loadSaveGameOptionsMenu); loadSaveGameOptionsMenu.ClientRectangle = new Rectangle(0, 0, 75, 0); loadSaveGameOptionsMenu.Items.Add(loadConfigMenuItem); loadSaveGameOptionsMenu.Items.Add(saveConfigMenuItem); btnSaveLoadGameOptions.LeftClick += (sender, args) => loadSaveGameOptionsMenu.Open(GetCursorPoint()); AddChild(loadSaveGameOptionsMenu); AddChild(loadOrSaveGameOptionPresetWindow); } } private void BtnMapSortAlphabetically_LeftClick(object sender, EventArgs e) { UserINISettings.Instance.MapSortState.Value = (int)btnMapSortAlphabetically.GetState(); RefreshMapSortAlphabeticallyBtn(); UserINISettings.Instance.SaveSettings(); ListMaps(); } private void RefreshMapSortAlphabeticallyBtn() { if (Enum.IsDefined(typeof(SortDirection), UserINISettings.Instance.MapSortState.Value)) btnMapSortAlphabetically.SetState((SortDirection)UserINISettings.Instance.MapSortState.Value); } private void MapLoader_MapChanged(object sender, MapChangedEventArgs e) { WindowManager.AddCallback(() => { switch (e.ChangeType) { case MapChangeType.Added: HandleMapAdded(e.Map); break; case MapChangeType.Updated: HandleMapUpdated(e.Map, e.PreviousMapSHA1); break; case MapChangeType.Removed: HandleMapRemoved(e.Map); break; } }, null); } protected virtual void HandleMapAdded(Map addedMap) { RefreshGameModeFilter(); if (ShouldShowMapInCurrentFilter(addedMap)) ListMaps(); } protected virtual void HandleMapUpdated(Map updatedMap, string previousSHA1) { // If the currently selected map was updated, refresh the UI if (Map != null && (Map.SHA1 == previousSHA1 || Map.SHA1 == updatedMap.SHA1)) { // Find the new GameModeMap for the updated map var updatedGameModeMap = GameModeMaps .FirstOrDefault(gmm => gmm.Map.SHA1 == updatedMap.SHA1); if (updatedGameModeMap != null) ChangeMap(updatedGameModeMap); } ListMaps(); } private void HandleMapRemoved(Map removedMap) { // If the currently selected map was removed, select a different one if (Map != null && Map.SHA1 == removedMap.SHA1) { var availableMaps = GameModeMaps.Where(gmm => gmm.GameMode == GameMode).ToList(); if (availableMaps.Any()) { ChangeMap(availableMaps.First()); } else { // No maps available for current game mode, change to a different one var firstAvailableGameModeMap = GameModeMaps.FirstOrDefault(); if (firstAvailableGameModeMap != null) { ChangeMap(firstAvailableGameModeMap); RefreshMapSelectionUI(); } } } RefreshGameModeFilter(); ListMaps(); } private bool ShouldShowMapInCurrentFilter(Map map) { if (map?.GameModes == null || gameModeMapFilter == null) return false; return map.GameModes.Any(gameModeName => { var gameMode = MapLoader.GameModes.FirstOrDefault(gm => gm.Name == gameModeName); if (gameMode == null) return false; return gameModeMapFilter.GetGameModeMaps().Any(gmm => gmm.GameMode.Name == gameMode.Name && gmm.Map.SHA1 == map.SHA1); }); } private static XNADropDownItem CreateGameFilterItem(string text, GameModeMapFilter filter) { return new XNADropDownItem { Text = text, Tag = filter }; } protected bool IsFavoriteMapsSelected() => ddGameModeMapFilter.SelectedItem?.Text == FavoriteMapsLabel; private List GetFavoriteGameModeMaps() => GameModeMaps.Where(gmm => gmm.IsFavorite).ToList(); private Func> GetGameModeMaps(GameMode gm) => () => GameModeMaps.Where(gmm => gmm.GameMode == gm).ToList(); private void RefreshBtnPlayerExtraOptionsOpenTexture() { if (btnPlayerExtraOptionsOpen != null) { var textureName = GetPlayerExtraOptions().IsDefault() ? "optionsButton.png" : "optionsButtonActive.png"; var hoverTextureName = GetPlayerExtraOptions().IsDefault() ? "optionsButton_c.png" : "optionsButtonActive_c.png"; var hoverTexture = AssetLoader.AssetExists(hoverTextureName) ? AssetLoader.LoadTexture(hoverTextureName) : null; btnPlayerExtraOptionsOpen.IdleTexture = AssetLoader.LoadTexture(textureName); btnPlayerExtraOptionsOpen.HoverTexture = hoverTexture; } } protected void HandleGameOptionPresetSaveCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetSaveCommand(e.PresetName); protected void HandleGameOptionPresetSaveCommand(string presetName) { string error = AddGameOptionPreset(presetName); if (!string.IsNullOrEmpty(error)) AddNotice(error); } protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommand(e.PresetName); protected void HandleGameOptionPresetLoadCommand(string presetName) { if (LoadGameOptionPreset(presetName)) AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); else AddNotice(string.Format("Preset {0} not found!".L10N("Client:Main:PresetNotFound"), presetName)); } protected void AddNotice(string message) => AddNotice(message, Color.White); protected abstract void AddNotice(string message, Color color); private void BtnPickRandomMap_LeftClick(object sender, EventArgs e) => PickRandomMap(); private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps(); private void TbMapSearch_RightClick(object sender, EventArgs e) => searchContextMenu.Open(GetCursorPoint()); private void SetSearchAllGameModes(bool value) { searchAllGameModes = value; UserINISettings.Instance.SearchAllGameModes.Value = value; UserINISettings.Instance.SaveSettings(); ListMaps(); } private void Dropdown_SelectedIndexChanged(object sender, EventArgs e) { if (disableGameOptionUpdateBroadcast) return; var dd = (GameLobbyDropDown)sender; dd.HostSelectedIndex = dd.SelectedIndex; OnGameOptionChanged(); } private void ChkBox_CheckedChanged(object sender, EventArgs e) { if (disableGameOptionUpdateBroadcast) return; var checkBox = (GameLobbyCheckBox)sender; checkBox.HostChecked = checkBox.Checked; OnGameOptionChanged(); } protected virtual void OnGameOptionChanged() { CheckDisallowedSides(); btnLaunchGame.SetRank(GetRank()); } protected void DdGameModeMapFilter_SelectedIndexChanged(object sender, EventArgs e) { gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; tbMapSearch.Text = string.Empty; tbMapSearch.OnSelectedChanged(); ListMaps(); if (lbGameModeMapList.SelectedIndex == -1) lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap else ChangeMap(GameModeMap); } protected void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) { if (PlayerExtraOptionsPanel.Enabled) PlayerExtraOptionsPanel.Disable(); else PlayerExtraOptionsPanel.Enable(); } protected void ApplyPlayerExtraOptions(string sender, string message) { var playerExtraOptions = PlayerExtraOptions.FromMessage(message); if (PlayerExtraOptionsPanel != null) { if (playerExtraOptions.IsForceRandomSides != PlayerExtraOptionsPanel.ForcedRandomSides) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomSides, "side selection".L10N("Client:Main:SideAsANoun")); if (playerExtraOptions.IsForceRandomColors != PlayerExtraOptionsPanel.ForcedRandomColors) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomColors, "color selection".L10N("Client:Main:ColorAsANoun")); if (playerExtraOptions.IsForceRandomStarts != PlayerExtraOptionsPanel.ForcedRandomStarts) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceRandomStarts, "start selection".L10N("Client:Main:StartPositionAsANoun")); if (playerExtraOptions.IsForceNoTeams != PlayerExtraOptionsPanel.ForcedNoTeams) AddPlayerExtraOptionForcedNotice(playerExtraOptions.IsForceNoTeams, "team selection".L10N("Client:Main:TeamAsANoun")); if (playerExtraOptions.IsUseTeamStartMappings != PlayerExtraOptionsPanel.UseTeamStartMappings) AddPlayerExtraOptionForcedNotice(!playerExtraOptions.IsUseTeamStartMappings, "auto ally".L10N("Client:Main:AutoAllyAsANoun")); } SetPlayerExtraOptions(playerExtraOptions); UpdateMapPreviewBoxEnabledStatus(); } private void AddPlayerExtraOptionForcedNotice(bool disabled, string type) => AddNotice(disabled ? string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableSection"), type) : string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableSection"), type)); protected List GetSortedGameModeMaps() { var gameModeMaps = searchAllGameModes ? GameModeMaps.ToList() : gameModeMapFilter.GetGameModeMaps(); // Only apply sort if the map list sort button is available. if (btnMapSortAlphabetically.Enabled && btnMapSortAlphabetically.Visible) { switch ((SortDirection)UserINISettings.Instance.MapSortState.Value) { case SortDirection.Asc: gameModeMaps = gameModeMaps.OrderBy(gmm => gmm.Map.Name).ToList(); break; case SortDirection.Desc: gameModeMaps = gameModeMaps.OrderByDescending(gmm => gmm.Map.Name).ToList(); break; } } return gameModeMaps; } protected void ListMaps() { lbGameModeMapList.SelectedIndexChanged -= LbGameModeMapList_SelectedIndexChanged; lbGameModeMapList.ClearItems(); lbGameModeMapList.SetTopIndex(0); lbGameModeMapList.SelectedIndex = -1; int mapIndex = -1; var isFavoriteMapsSelected = IsFavoriteMapsSelected(); var maps = GetSortedGameModeMaps(); bool gameModeMapChanged = false; List filteredMaps; if (tbMapSearch.Text != tbMapSearch.Suggestion) { string search = tbMapSearch.Text.Trim(); string[] searchWords = search.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // Equals entire search string var exactMatches = maps.Where(gmm => gmm.Map.Name.Equals(search, StringComparison.CurrentCultureIgnoreCase) || gmm.Map.UntranslatedName.Equals(search, StringComparison.InvariantCultureIgnoreCase)).ToList(); // Contains entire search string var substringMatches = maps.Except(exactMatches).Where(gmm => gmm.Map.Name.Contains(search, StringComparison.CurrentCultureIgnoreCase) || gmm.Map.UntranslatedName.Contains(search, StringComparison.InvariantCultureIgnoreCase)).ToList(); // Contains all search words. It matches with "AND" logic: Word1 AND Word2 AND Word3 var multiWordMatches = maps.Except(exactMatches).Except(substringMatches).Where(gmm => { bool allInTranslated = searchWords.All(word => gmm.Map.Name.Contains(word, StringComparison.CurrentCultureIgnoreCase)); bool allInUntranslated = searchWords.All(word => gmm.Map.UntranslatedName.Contains(word, StringComparison.InvariantCultureIgnoreCase)); return allInTranslated || allInUntranslated; }).ToList(); filteredMaps = [.. exactMatches, .. substringMatches, .. multiWordMatches]; } else { filteredMaps = maps; } for (int i = 0; i < filteredMaps.Count; i++) { var gameModeMap = filteredMaps[i]; XNAListBoxItem rankItem = new XNAListBoxItem(); if (gameModeMap.IsCoop) { // Note: StatisticsManager.Statistics must be initialized to call `HasBeatCoOpMap()`. This means StatisticsWindow must be initialized before any lobbies extending GameLobbyBase. if (StatisticsManager.Instance.HasBeatCoOpMap(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName)) rankItem.Texture = RankTextures[Math.Abs(2 - gameModeMap.CoopDifficultyLevel) + 1]; else rankItem.Texture = RankTextures[0]; } else rankItem.Texture = RankTextures[GetDefaultMapRankIndex(gameModeMap) + 1]; XNAListBoxItem mapNameItem = new XNAListBoxItem(); var mapNameText = gameModeMap.Map.Name; if (isFavoriteMapsSelected || searchAllGameModes) mapNameText += $" - {gameModeMap.GameMode.UIName}"; mapNameItem.Text = Renderer.GetSafeString(mapNameText, lbGameModeMapList.FontIndex); if (gameModeMap.MultiplayerOnly && !isMultiplayer) mapNameItem.TextColor = UISettings.ActiveSettings.DisabledItemColor; mapNameItem.Tag = gameModeMap; XNAListBoxItem[] mapInfoArray = { rankItem, mapNameItem, }; lbGameModeMapList.AddItem(mapInfoArray); // Preserve the selected map if (gameModeMap == GameModeMap) { mapIndex = i; gameModeMapChanged = false; } if (mapIndex == -1 && (gameModeMap?.Map?.Equals(GameModeMap?.Map) ?? false)) { mapIndex = i; gameModeMapChanged = true; } } if (mapIndex > -1) { lbGameModeMapList.SelectedIndex = mapIndex; while (mapIndex > lbGameModeMapList.LastIndex) lbGameModeMapList.TopIndex++; } lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged; // Trigger the event manually to update GameModeMap if (gameModeMapChanged) LbGameModeMapList_SelectedIndexChanged(); } protected abstract int GetDefaultMapRankIndex(GameModeMap gameModeMap); private void LbGameModeMapList_RightClick(object sender, EventArgs e) { if (lbGameModeMapList.HoveredIndex < 0 || lbGameModeMapList.HoveredIndex >= lbGameModeMapList.ItemCount) return; lbGameModeMapList.SelectedIndex = lbGameModeMapList.HoveredIndex; if (!mapContextMenu.Items.Any(i => i.VisibilityChecker == null || i.VisibilityChecker())) return; toggleFavoriteItem.Text = GameModeMap.IsFavorite ? "Remove Favorite".L10N("Client:Main:RemoveFavorite") : "Add Favorite".L10N("Client:Main:AddFavorite"); mapContextMenu.Open(GetCursorPoint()); } private bool CanDeleteMap() { return Map != null && !Map.Official && !isMultiplayer; } private void DeleteMapConfirmation() { if (Map == null) return; var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Delete Confirmation".L10N("Client:Main:DeleteMapConfirmTitle"), string.Format("Are you sure you wish to delete the custom map {0}?".L10N("Client:Main:DeleteMapConfirmText"), Map.Name)); messageBox.YesClickedAction = DeleteSelectedMap; } private void ShowInFolder() => Map?.OpenContainingFolder(); private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) => ToggleFavoriteMap(); protected virtual void ToggleFavoriteMap() { if (GameModeMap != null) { GameModeMap.IsFavorite = UserINISettings.Instance.ToggleFavoriteMap(Map.SHA1, GameMode.Name, GameModeMap.IsFavorite); MapPreviewBox.RefreshFavoriteBtn(); } } protected void RefreshForFavoriteMapRemoved() { if (!gameModeMapFilter.GetGameModeMaps().Any()) { LoadDefaultGameModeMap(); return; } ListMaps(); if (IsFavoriteMapsSelected()) lbGameModeMapList.SelectedIndex = 0; // the map was removed while viewing favorites } private void DeleteSelectedMap(XNAMessageBox messageBox) { try { MapLoader.DeleteCustomMap(GameModeMap); tbMapSearch.Text = string.Empty; if (GameMode.Maps.Count == 0) { // this will trigger another GameMode to be selected GameModeMap = GameModeMaps.FirstOrDefault(gm => gm.GameMode.Maps.Count > 0); } else { // this will trigger another Map to be selected lbGameModeMapList.SelectedIndex = lbGameModeMapList.SelectedIndex == 0 ? 1 : lbGameModeMapList.SelectedIndex - 1; } ListMaps(); ChangeMap(GameModeMap); } catch (IOException ex) { Logger.Log($"Deleting map {Map.BaseFilePath} failed! Message: {ex.ToString()}"); XNAMessageBox.Show(WindowManager, "Deleting Map Failed".L10N("Client:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("Client:Main:DeleteMapFailedText") + " " + ex.Message); } } private void LbGameModeMapList_SelectedIndexChanged() { if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) { ChangeMap(null); return; } XNAListBoxItem item = lbGameModeMapList.GetItem(1, lbGameModeMapList.SelectedIndex); GameModeMap gameModeMap = (GameModeMap)item.Tag; ChangeMap(gameModeMap); } private void LbGameModeMapList_SelectedIndexChanged(object sender, EventArgs e) => LbGameModeMapList_SelectedIndexChanged(); private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) { if (lbGameModeMapList.HoveredIndex < 0 || lbGameModeMapList.HoveredIndex >= lbGameModeMapList.ItemCount) { mapListTooltip.Text = string.Empty; return; } var gmm = (GameModeMap)lbGameModeMapList.GetItem(1, lbGameModeMapList.HoveredIndex).Tag; if (gmm.Map.UntranslatedName != gmm.Map.Name) mapListTooltip.Text = "Original name:".L10N("Client:Main:OriginalMapName") + " " + gmm.Map.UntranslatedName; else mapListTooltip.Text = string.Empty; } private void PickRandomMap() { int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; List maps = GetMapList(totalPlayerCount); if (maps.Count < 1) return; int randomValue = random.Next(0, maps.Count); bool isFavoriteMapsSelected = IsFavoriteMapsSelected(); GameModeMap = GameModeMaps.FirstOrDefault(gmm => (gmm.GameMode == GameMode || gmm.IsFavorite && isFavoriteMapsSelected) && gmm.Map == maps[randomValue]); Logger.Log("PickRandomMap: Rolled " + randomValue + " out of " + maps.Count + ". Picked map: " + Map.Name); ChangeMap(GameModeMap); tbMapSearch.Text = string.Empty; tbMapSearch.OnSelectedChanged(); ListMaps(); } private List GetMapList(int playerCount) { List maps = IsFavoriteMapsSelected() ? GetFavoriteGameModeMaps().Select(gameModeMap => gameModeMap.Map).ToList() : GameMode?.Maps.ToList() ?? new List(); if (playerCount != 1) { if (GameMode?.MaxPlayersOverride != null) { // MaxPlayers have been overridden in GameMode. This means all maps in the game mode has the same MaxPlayers value if (playerCount != GameMode.MaxPlayersOverride) maps = []; } else { // Maps could have different MaxPlayers values. maps = maps.Where(x => x.MaxPlayers == playerCount).ToList(); } if (maps.Count < 1 && playerCount <= MAX_PLAYER_COUNT) return GetMapList(playerCount + 1); } return maps; } /// /// Refreshes the game mode filter dropdown to include all current game modes. /// protected void RefreshGameModeFilter() { string currentSelection = ddGameModeMapFilter.SelectedItem?.Text; ddGameModeMapFilter.SelectedIndexChanged -= DdGameModeMapFilter_SelectedIndexChanged; ddGameModeMapFilter.Items.Clear(); ddGameModeMapFilter.AddItem(CreateGameFilterItem(FavoriteMapsLabel, new GameModeMapFilter(GetFavoriteGameModeMaps))); foreach (GameMode gm in GameModeMaps.GameModes) ddGameModeMapFilter.AddItem(CreateGameFilterItem(gm.UIName, new GameModeMapFilter(GetGameModeMaps(gm)))); int selectedIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Text == currentSelection); ddGameModeMapFilter.SelectedIndex = selectedIndex >= 0 ? selectedIndex : 0; ddGameModeMapFilter.SelectedIndexChanged += DdGameModeMapFilter_SelectedIndexChanged; gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; } /// /// Refreshes the map selection UI to match the currently selected map /// and game mode. /// protected void RefreshMapSelectionUI() { if (GameMode == null) return; int gameModeMapFilterIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Text == GameMode.UIName); if (gameModeMapFilterIndex == -1) return; if (ddGameModeMapFilter.SelectedIndex == gameModeMapFilterIndex) DdGameModeMapFilter_SelectedIndexChanged(this, EventArgs.Empty); ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex; } protected void AddSideToDropDown(XNADropDown dd, string name, string? uiName = null, Texture2D? texture = null) { XNADropDownItem item = new() { Text = uiName ?? name.L10N($"INI:Sides:{name}"), Tag = name, Texture = texture ?? LoadTextureOrNull(name + "icon.png"), }; dd.AddItem(item); } /// /// Initializes the player option drop-down controls. /// protected void InitPlayerOptionDropdowns() { ddPlayerNames = new XNAClientDropDown[MAX_PLAYER_COUNT]; ddPlayerSides = new XNAClientDropDown[MAX_PLAYER_COUNT]; ddPlayerColors = new XNAClientColorDropDown[MAX_PLAYER_COUNT]; ddPlayerStarts = new XNAClientDropDown[MAX_PLAYER_COUNT]; ddPlayerTeams = new XNAClientDropDown[MAX_PLAYER_COUNT]; int playerOptionVecticalMargin = ConfigIni.GetIntValue(Name, "PlayerOptionVerticalMargin", PLAYER_OPTION_VERTICAL_MARGIN); int playerOptionHorizontalMargin = ConfigIni.GetIntValue(Name, "PlayerOptionHorizontalMargin", PLAYER_OPTION_HORIZONTAL_MARGIN); int playerOptionCaptionLocationY = ConfigIni.GetIntValue(Name, "PlayerOptionCaptionLocationY", PLAYER_OPTION_CAPTION_Y); int playerNameWidth = ConfigIni.GetIntValue(Name, "PlayerNameWidth", 136); int sideWidth = ConfigIni.GetIntValue(Name, "SideWidth", 91); int colorWidth = ConfigIni.GetIntValue(Name, "ColorWidth", 79); int startWidth = ConfigIni.GetIntValue(Name, "StartWidth", 49); int teamWidth = ConfigIni.GetIntValue(Name, "TeamWidth", 46); int locationX = ConfigIni.GetIntValue(Name, "PlayerOptionLocationX", 25); int locationY = ConfigIni.GetIntValue(Name, "PlayerOptionLocationY", 24); // InitPlayerOptionDropdowns(136, 91, 79, 49, 46, new Point(25, 24)); string[] sides = ClientConfiguration.Instance.Sides.Split(',').ToArray(); SideCount = sides.Length; List selectorNames = new(); GetRandomSelectors(selectorNames, RandomSelectors); RandomSelectorCount = RandomSelectors.Count + 1; MapPreviewBox.RandomSelectorCount = RandomSelectorCount; string randomColor = GameOptionsIni.GetStringValue("General", "RandomColor", "255,255,255"); for (int i = MAX_PLAYER_COUNT - 1; i > -1; i--) { var ddPlayerName = new XNAClientDropDown(WindowManager); ddPlayerName.Name = "ddPlayerName" + i; ddPlayerName.ClientRectangle = new Rectangle(locationX, locationY + (DROP_DOWN_HEIGHT + playerOptionVecticalMargin) * i, playerNameWidth, DROP_DOWN_HEIGHT); ddPlayerName.AddItem(String.Empty); ProgramConstants.AI_PLAYER_NAMES.ForEach(ddPlayerName.AddItem); ddPlayerName.AllowDropDown = true; ddPlayerName.SelectedIndexChanged += CopyPlayerDataFromUI; ddPlayerName.RightClick += MultiplayerName_RightClick; ddPlayerName.Tag = true; var ddPlayerSide = new XNAClientDropDown(WindowManager); ddPlayerSide.Name = "ddPlayerSide" + i; ddPlayerSide.ClientRectangle = new Rectangle( ddPlayerName.Right + playerOptionHorizontalMargin, ddPlayerName.Y, sideWidth, DROP_DOWN_HEIGHT); const string randomName = "Random"; AddSideToDropDown(ddPlayerSide, randomName, randomName.L10N("Client:Sides:RandomSide"), LoadTextureOrNull("randomicon.png")); foreach (string randomSelector in selectorNames) AddSideToDropDown(ddPlayerSide, randomSelector); foreach (string sideName in sides) AddSideToDropDown(ddPlayerSide, sideName); ddPlayerSide.AllowDropDown = false; ddPlayerSide.SelectedIndexChanged += CopyPlayerDataFromUI; ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientColorDropDown(WindowManager); ddPlayerColor.Name = "ddPlayerColor" + i; ddPlayerColor.ClientRectangle = new Rectangle( ddPlayerSide.Right + playerOptionHorizontalMargin, ddPlayerName.Y, colorWidth, DROP_DOWN_HEIGHT); ddPlayerColor.AddItem("Random".L10N("Client:Main:RandomColor"), AssetLoader.GetColorFromString(randomColor)); foreach (MultiplayerColor mpColor in MPColors) ddPlayerColor.AddItem(mpColor.Name, mpColor.XnaColor); ddPlayerColor.AllowDropDown = false; ddPlayerColor.SelectedIndexChanged += CopyPlayerDataFromUI; ddPlayerColor.Tag = false; var ddPlayerTeam = new XNAClientDropDown(WindowManager); ddPlayerTeam.Name = "ddPlayerTeam" + i; ddPlayerTeam.ClientRectangle = new Rectangle( ddPlayerColor.Right + playerOptionHorizontalMargin, ddPlayerName.Y, teamWidth, DROP_DOWN_HEIGHT); ddPlayerTeam.AddItem("-"); ProgramConstants.TEAMS.ForEach(ddPlayerTeam.AddItem); ddPlayerTeam.AllowDropDown = false; ddPlayerTeam.SelectedIndexChanged += CopyPlayerDataFromUI; ddPlayerTeam.Tag = true; var ddPlayerStart = new XNAClientDropDown(WindowManager); ddPlayerStart.Name = "ddPlayerStart" + i; ddPlayerStart.ClientRectangle = new Rectangle( ddPlayerTeam.Right + playerOptionHorizontalMargin, ddPlayerName.Y, startWidth, DROP_DOWN_HEIGHT); for (int j = 1; j <= MAX_PLAYER_COUNT; j++) ddPlayerStart.AddItem(j.ToString()); ddPlayerStart.AllowDropDown = false; ddPlayerStart.SelectedIndexChanged += CopyPlayerDataFromUI; ddPlayerStart.Visible = false; ddPlayerStart.Enabled = false; ddPlayerStart.Tag = true; ddPlayerNames[i] = ddPlayerName; ddPlayerSides[i] = ddPlayerSide; ddPlayerColors[i] = ddPlayerColor; ddPlayerStarts[i] = ddPlayerStart; ddPlayerTeams[i] = ddPlayerTeam; PlayerOptionsPanel.AddChild(ddPlayerName); PlayerOptionsPanel.AddChild(ddPlayerSide); PlayerOptionsPanel.AddChild(ddPlayerColor); PlayerOptionsPanel.AddChild(ddPlayerStart); PlayerOptionsPanel.AddChild(ddPlayerTeam); ReadINIForControl(ddPlayerName); ReadINIForControl(ddPlayerSide); ReadINIForControl(ddPlayerColor); ReadINIForControl(ddPlayerStart); ReadINIForControl(ddPlayerTeam); } var lblName = GeneratePlayerOptionCaption("lblName", "PLAYER".L10N("Client:Main:PlayerOptionPlayer"), ddPlayerNames[0].X, playerOptionCaptionLocationY); var lblSide = GeneratePlayerOptionCaption("lblSide", "SIDE".L10N("Client:Main:PlayerOptionSide"), ddPlayerSides[0].X, playerOptionCaptionLocationY); var lblColor = GeneratePlayerOptionCaption("lblColor", "COLOR".L10N("Client:Main:PlayerOptionColor"), ddPlayerColors[0].X, playerOptionCaptionLocationY); var lblStart = GeneratePlayerOptionCaption("lblStart", "START".L10N("Client:Main:PlayerOptionStart"), ddPlayerStarts[0].X, playerOptionCaptionLocationY); lblStart.Visible = false; var lblTeam = GeneratePlayerOptionCaption("lblTeam", "TEAM".L10N("Client:Main:PlayerOptionTeam"), ddPlayerTeams[0].X, playerOptionCaptionLocationY); ReadINIForControl(lblName); ReadINIForControl(lblSide); ReadINIForControl(lblColor); ReadINIForControl(lblStart); ReadINIForControl(lblTeam); btnPlayerExtraOptionsOpen = FindChild(nameof(btnPlayerExtraOptionsOpen), true); if (btnPlayerExtraOptionsOpen != null) { PlayerExtraOptionsPanel = FindChild(nameof(PlayerExtraOptionsPanel)); ReadINIForControl(PlayerExtraOptionsPanel); foreach (var child in PlayerExtraOptionsPanel.Children) { ReadINIForControl(child); } PlayerExtraOptionsPanel.Disable(); PlayerExtraOptionsPanel.OptionsChanged += PlayerExtraOptions_OptionsChanged; btnPlayerExtraOptionsOpen.LeftClick += BtnPlayerExtraOptions_LeftClick; } CheckDisallowedSides(); } private XNALabel GeneratePlayerOptionCaption(string name, string text, int x, int y) { var label = new XNALabel(WindowManager); label.Name = name; label.Text = text; label.FontIndex = 1; label.ClientRectangle = new Rectangle(x, y, 0, 0); PlayerOptionsPanel.AddChild(label); return label; } protected virtual void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e) { var playerExtraOptions = GetPlayerExtraOptions(); for (int i = 0; i < MAX_PLAYER_COUNT; i++) { var pInfo = GetPlayerInfoForIndex(i); // IsForceRandomSides if (pInfo != null && playerExtraOptions.IsForceRandomSides) pInfo.SideId = 0; EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides); // IsForceNoTeams Debug.Assert(!playerExtraOptions.IsForceNoTeams || !GameModeMap.IsCoop, "Co-ops should not have force no teams enabled."); if (pInfo != null && playerExtraOptions.IsForceNoTeams) pInfo.TeamId = 0; EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceNoTeams); // IsForceRandomColors if (pInfo != null && playerExtraOptions.IsForceRandomColors) pInfo.ColorId = 0; EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors); // IsForceRandomStarts if (pInfo != null && playerExtraOptions.IsForceRandomStarts) pInfo.StartingLocation = 0; EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts); } CopyPlayerDataToUI(); RefreshBtnPlayerExtraOptionsOpenTexture(); } private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int playerIndex, bool enable) { var pInfo = GetPlayerInfoForIndex(playerIndex); var allowOtherPlayerOptionsChange = AllowPlayerOptionsChange() && pInfo != null; clientDropDown.AllowDropDown = enable && (allowOtherPlayerOptionsChange || pInfo?.Name == ProgramConstants.PLAYERNAME); } protected PlayerInfo GetPlayerInfoForIndex(int playerIndex) { if (playerIndex < Players.Count) return Players[playerIndex]; if (playerIndex < Players.Count + AIPlayers.Count) return AIPlayers[playerIndex - Players.Count]; return null; } protected PlayerExtraOptions GetPlayerExtraOptions() => PlayerExtraOptionsPanel == null ? new PlayerExtraOptions() : PlayerExtraOptionsPanel.GetPlayerExtraOptions(); protected void SetPlayerExtraOptions(PlayerExtraOptions playerExtraOptions) => PlayerExtraOptionsPanel?.SetPlayerExtraOptions(playerExtraOptions); protected string GetTeamMappingsError() => GetPlayerExtraOptions()?.GetTeamMappingsError(); private Texture2D LoadTextureOrNull(string name) => AssetLoader.AssetExists(name) ? AssetLoader.LoadTexture(name) : null; /// /// Loads random side selectors from GameOptions.ini. /// /// The UI names of random selectors. /// The side IDs to choose from for the selectors. private void GetRandomSelectors(List selectorNames, List selectorSides) { List keys = GameOptionsIni.GetSectionKeys("RandomSelectors"); if (keys == null) return; foreach (string randomSelector in keys) { List randomSides = new List(); try { string[] tmp = GameOptionsIni.GetStringListValue("RandomSelectors", randomSelector, string.Empty); randomSides = Array.ConvertAll(tmp, int.Parse).ToList(); randomSides.RemoveAll(x => (x >= SideCount || x < 0)); } catch (FormatException) { } if (randomSides.Count > 1) { selectorNames.Add(randomSelector); selectorSides.Add(randomSides.ToArray()); } } } protected abstract void BtnLaunchGame_LeftClick(object sender, EventArgs e); protected abstract void BtnLeaveGame_LeftClick(object sender, EventArgs e); /// /// Updates Discord Rich Presence with actual information. /// /// Whether to restart the "Elapsed" timer or not protected abstract void UpdateDiscordPresence(bool resetTimer = false); /// /// Resets Discord Rich Presence to default state. /// protected void ResetDiscordPresence() => discordHandler.UpdatePresence(); protected void LoadDefaultGameModeMap() { if (ddGameModeMapFilter.Items.Count > 0) { ddGameModeMapFilter.SelectedIndex = GetDefaultGameModeMapFilterIndex(); lbGameModeMapList.SelectedIndex = 0; } } protected int GetDefaultGameModeMapFilterIndex() { int firstNonEmptyFilter = ddGameModeMapFilter.Items.FindIndex(i => (i.Tag as GameModeMapFilter)?.Any() ?? false); if (firstNonEmptyFilter == -1) firstNonEmptyFilter = 0; return firstNonEmptyFilter; } protected GameModeMapFilter GetDefaultGameModeMapFilter() { return ddGameModeMapFilter.Items[GetDefaultGameModeMapFilterIndex()].Tag as GameModeMapFilter; } private int GetSpectatorSideIndex() => SideCount + RandomSelectorCount; /// /// Applies disallowed side indexes to the side option drop-downs /// and player options for human or computer players. /// protected void CheckDisallowedSidesForGroup(bool forHumanPlayers) { var disallowedSideArray = GetDisallowedSidesForGroup(forHumanPlayers); var playerInfos = forHumanPlayers ? Players : AIPlayers; int defaultSide = 0; int allowedSideCount = disallowedSideArray.Count(b => b == false); if (allowedSideCount == 1) { // Disallow Random for (int i = 0; i < disallowedSideArray.Length; i++) { if (!disallowedSideArray[i]) defaultSide = i + RandomSelectorCount; } foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; for (int i = 0; i < RandomSelectorCount; i++) dd.Items[i].Selectable = false; } } else { foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; for (int i = 0; i < RandomSelectorCount; i++) dd.Items[i].Selectable = true; } } // Disable custom random groups if all or all except one of included sides are unavailable. int c = 0; foreach (int[] randomSides in RandomSelectors) { int disableCount = 0; foreach (int side in randomSides) { if (disallowedSideArray[side]) disableCount++; } bool disabled = disableCount >= randomSides.Length - 1; foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; dd.Items[1 + c].Selectable = !disabled; if (pInfo.SideId == 1 + c && disabled) pInfo.SideId = defaultSide; } c++; } // Go over the side array and either disable or enable the side // dropdown options depending on whether the side is available for (int i = 0; i < disallowedSideArray.Length; i++) { bool disabled = disallowedSideArray[i]; if (disabled) { // Change the sides of players that use the disabled // side to the default side foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; dd.Items[i + RandomSelectorCount].Selectable = false; if (pInfo.SideId == i + RandomSelectorCount) pInfo.SideId = defaultSide; } } else { foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; dd.Items[i + RandomSelectorCount].Selectable = true; } } } // If only 1 side is allowed, change all players' sides to that if (allowedSideCount == 1) { foreach (PlayerInfo pInfo in playerInfos) { if (pInfo.SideId == 0) pInfo.SideId = defaultSide; } } if (GameModeMap != null && GameModeMap.CoopInfo != null) { // Disallow spectator foreach (PlayerInfo pInfo in playerInfos) { if (pInfo.SideId == GetSpectatorSideIndex()) pInfo.SideId = defaultSide; } foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; if (dd.Items.Count > GetSpectatorSideIndex()) dd.Items[SideCount + RandomSelectorCount].Selectable = false; } } else { foreach (PlayerInfo pInfo in playerInfos) { var dd = ddPlayerSides[pInfo.Index]; if (dd.Items.Count > SideCount + RandomSelectorCount) dd.Items[SideCount + RandomSelectorCount].Selectable = true; } } } /// /// Applies disallowed side indexes to the side option drop-downs /// and player options. /// protected void CheckDisallowedSides() { CheckDisallowedSidesForGroup(forHumanPlayers: false); CheckDisallowedSidesForGroup(forHumanPlayers: true); } /// /// Gets a list of side indexes that are disallowed for human or computer players. /// /// A list of disallowed side indexes. protected bool[] GetDisallowedSidesForGroup(bool forHumanPlayers) { var returnValue = GetDisallowedSides(); var sides = forHumanPlayers ? GameMode?.DisallowedHumanPlayerSides : GameMode?.DisallowedComputerPlayerSides; if (sides != null) { foreach (int i in sides) returnValue[i] = true; } return returnValue; } /// /// Gets a list of side indexes that are disallowed. /// /// A list of disallowed side indexes. protected bool[] GetDisallowedSides() { var returnValue = new bool[SideCount]; if (GameModeMap != null && GameModeMap.CoopInfo != null) { // Co-Op map disallowed side logic foreach (int disallowedSideIndex in GameModeMap.CoopInfo.DisallowedPlayerSides) returnValue[disallowedSideIndex] = true; } if (GameMode != null) { foreach (int disallowedSideIndex in GameMode.DisallowedPlayerSides) returnValue[disallowedSideIndex] = true; } foreach (var checkBox in CheckBoxes) checkBox.ApplyDisallowedSideIndex(returnValue); return returnValue; } /// /// Randomizes options of both human and AI players /// and returns the options as an array of PlayerHouseInfos. /// /// An array of PlayerHouseInfos. protected virtual PlayerHouseInfo[] Randomize(List teamStartMappings, Random pseudoRandom) { int totalPlayerCount = Players.Count + AIPlayers.Count; PlayerHouseInfo[] houseInfos = new PlayerHouseInfo[totalPlayerCount]; for (int i = 0; i < totalPlayerCount; i++) houseInfos[i] = new PlayerHouseInfo(); // Gather list of spectators for (int i = 0; i < Players.Count; i++) houseInfos[i].IsSpectator = Players[i].SideId == GetSpectatorSideIndex(); // Gather list of available colors List freeColors = new List(); for (int cId = 0; cId < MPColors.Count; cId++) freeColors.Add(cId); if (GameModeMap.CoopInfo != null) { foreach (int colorIndex in GameModeMap.CoopInfo.DisallowedPlayerColors) freeColors.Remove(colorIndex); } foreach (PlayerInfo player in Players) freeColors.Remove(player.ColorId - 1); // The first color is Random foreach (PlayerInfo aiPlayer in AIPlayers) freeColors.Remove(aiPlayer.ColorId - 1); // Gather list of available starting locations List freeStartingLocations = new List(); List takenStartingLocations = new List(); foreach (int i in GameModeMap.AllowedStartingLocations) freeStartingLocations.Add(i - 1); for (int i = 0; i < Players.Count; i++) { if (!houseInfos[i].IsSpectator) { freeStartingLocations.Remove(Players[i].StartingLocation - 1); //takenStartingLocations.Add(Players[i].StartingLocation - 1); // ^ Gives everyone with a selected location a completely random // location in-game, because PlayerHouseInfo.RandomizeStart already // fills the list itself } } for (int i = 0; i < AIPlayers.Count; i++) freeStartingLocations.Remove(AIPlayers[i].StartingLocation - 1); foreach (var teamStartMapping in teamStartMappings.Where(mapping => mapping.IsBlock)) freeStartingLocations.Remove(teamStartMapping.StartingWaypoint); // Randomize options for (int i = 0; i < totalPlayerCount; i++) { PlayerInfo pInfo; PlayerHouseInfo pHouseInfo = houseInfos[i]; bool[] disallowedSides; if (i < Players.Count) { pInfo = Players[i]; disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers: true); } else { pInfo = AIPlayers[i - Players.Count]; disallowedSides = GetDisallowedSidesForGroup(forHumanPlayers: false); } pHouseInfo.RandomizeSide(pInfo, SideCount, pseudoRandom, disallowedSides, RandomSelectors, RandomSelectorCount); pHouseInfo.RandomizeColor(pInfo, freeColors, MPColors, pseudoRandom); bool overrideGameRandomLocations = teamStartMappings.Any() || GameModeMap.AllowedStartingLocations.Max() > GameModeMap.MaxPlayers; // non-sequential AllowedStartingLocations pHouseInfo.RandomizeStart(pInfo, pseudoRandom, freeStartingLocations, takenStartingLocations, overrideGameRandomLocations); } return houseInfos; } /// /// Writes spawn.ini. Returns the player house info returned from the randomizer. /// private PlayerHouseInfo[] WriteSpawnIni(Random pseudoRandom) { Logger.Log("Writing spawn.ini"); FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); spawnerSettingsFile.Delete(); if (GameModeMap.IsCoop) { foreach (PlayerInfo pInfo in Players) { Debug.Assert(pInfo.TeamId == 1, "Co-ops should always set TeamId to 1 before lanching the game"); pInfo.TeamId = 1; } foreach (PlayerInfo pInfo in AIPlayers) { Debug.Assert(pInfo.TeamId == 1, "Co-ops should always set TeamId to 1 before lanching the game"); pInfo.TeamId = 1; } } var teamStartMappings = new List(0); if (PlayerExtraOptionsPanel != null) { teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings(); } PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings, pseudoRandom); IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName); IniSection settings = new IniSection("Settings"); settings.SetStringValue("Name", ProgramConstants.PLAYERNAME); settings.SetStringValue("Scenario", ProgramConstants.SPAWNMAP_INI); settings.SetStringValue("UIGameMode", GameMode.UntranslatedUIName); settings.SetStringValue("UIMapName", Map.UntranslatedName); // needed for translation in game loading lobbies if (Map.Official) settings.SetStringValue("MapID", Map.BaseFilePath); settings.SetIntValue("PlayerCount", Players.Count); int myIndex = Players.FindIndex(c => c.Name == ProgramConstants.PLAYERNAME); settings.SetIntValue("Side", houseInfos[myIndex].InternalSideIndex); settings.SetBooleanValue("IsSpectator", houseInfos[myIndex].IsSpectator); settings.SetIntValue("Color", houseInfos[myIndex].ColorIndex); settings.SetStringValue("CustomLoadScreen", LoadingScreenController.GetLoadScreenName(houseInfos[myIndex].InternalSideIndex.ToString())); settings.SetIntValue("AIPlayers", AIPlayers.Count); settings.SetIntValue("Seed", RandomSeed); if (GetPvPTeamCount() > 1) settings.SetBooleanValue("CoachMode", true); if (GetGameType() == GameType.Coop) settings.SetBooleanValue("AutoSurrender", false); spawnIni.AddSection(settings); WriteSpawnIniAdditions(spawnIni); foreach (GameLobbyCheckBox chkBox in CheckBoxes) chkBox.ApplySpawnIniCode(spawnIni); foreach (GameLobbyDropDown dd in DropDowns) dd.ApplySpawnIniCode(spawnIni); // Apply forced options from GameOptions.ini List forcedKeys = GameOptionsIni.GetSectionKeys("ForcedSpawnIniOptions"); if (forcedKeys != null) { foreach (string key in forcedKeys) { spawnIni.SetStringValue("Settings", key, GameOptionsIni.GetStringValue("ForcedSpawnIniOptions", key, String.Empty)); } } GameMode.ApplySpawnIniCode(spawnIni); // Forced options from the game mode Map.ApplySpawnIniCode(spawnIni, Players.Count + AIPlayers.Count, AIPlayers.Count, GameModeMap.IsCoop, GameModeMap.CoopInfo, GameModeMap.CoopDifficultyLevel, pseudoRandom, SideCount); // Forced options from the map // Player options int otherId = 1; for (int pId = 0; pId < Players.Count; pId++) { PlayerInfo pInfo = Players[pId]; PlayerHouseInfo pHouseInfo = houseInfos[pId]; if (pInfo.Name == ProgramConstants.PLAYERNAME) continue; string sectionName = "Other" + otherId; spawnIni.SetStringValue(sectionName, "Name", pInfo.Name); spawnIni.SetIntValue(sectionName, "Side", pHouseInfo.InternalSideIndex); spawnIni.SetBooleanValue(sectionName, "IsSpectator", pHouseInfo.IsSpectator); spawnIni.SetIntValue(sectionName, "Color", pHouseInfo.ColorIndex); spawnIni.SetStringValue(sectionName, "Ip", GetIPAddressForPlayer(pInfo)); spawnIni.SetIntValue(sectionName, "Port", pInfo.Port); otherId++; } // The spawner assigns players to SpawnX houses based on their in-game color index List multiCmbIndexes = new List(); var sortedColorList = MPColors.OrderBy(mpc => mpc.GameColorIndex).ToList(); for (int cId = 0; cId < sortedColorList.Count; cId++) { for (int pId = 0; pId < Players.Count; pId++) { if (houseInfos[pId].ColorIndex == sortedColorList[cId].GameColorIndex) multiCmbIndexes.Add(pId); } } if (AIPlayers.Count > 0) { for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { int multiId = multiCmbIndexes.Count + aiId + 1; string keyName = "Multi" + multiId; spawnIni.SetIntValue("HouseHandicaps", keyName, AIPlayers[aiId].HouseHandicapAILevel); spawnIni.SetIntValue("HouseCountries", keyName, houseInfos[Players.Count + aiId].InternalSideIndex); spawnIni.SetIntValue("HouseColors", keyName, houseInfos[Players.Count + aiId].ColorIndex); } } for (int multiId = 0; multiId < multiCmbIndexes.Count; multiId++) { int pIndex = multiCmbIndexes[multiId]; if (houseInfos[pIndex].IsSpectator) spawnIni.SetBooleanValue("IsSpectator", "Multi" + (multiId + 1), true); } // Write alliances, the code is pretty big so let's take it to another class AllianceHolder.WriteInfoToSpawnIni(Players, AIPlayers, multiCmbIndexes, houseInfos.ToList(), teamStartMappings, spawnIni); for (int pId = 0; pId < Players.Count; pId++) { int startingWaypoint = houseInfos[multiCmbIndexes[pId]].StartingWaypoint; // -1 means no starting location at all - let the game itself pick the starting location // using its own logic if (startingWaypoint > -1) { int multiIndex = pId + 1; spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, startingWaypoint); } } for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { int startingWaypoint = houseInfos[Players.Count + aiId].StartingWaypoint; if (startingWaypoint > -1) { int multiIndex = Players.Count + aiId + 1; spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, startingWaypoint); } } spawnIni.WriteIniFile(); return houseInfos; } /// /// Returns the number of teams with human players in them. /// Does not count spectators and human players that don't have a team set. /// /// The number of human player teams in the game. private int GetPvPTeamCount() { int[] teamPlayerCounts = new int[4]; int playerTeamCount = 0; foreach (PlayerInfo pInfo in Players) { if (pInfo.IsAI || IsPlayerSpectator(pInfo)) continue; if (pInfo.TeamId > 0) { teamPlayerCounts[pInfo.TeamId - 1]++; if (teamPlayerCounts[pInfo.TeamId - 1] == 2) playerTeamCount++; } } return playerTeamCount; } /// /// Checks whether the specified player has selected Spectator as their side. /// /// The player. /// True if the player is a spectator, otherwise false. protected bool IsPlayerSpectator(PlayerInfo pInfo) { if (pInfo.SideId == GetSpectatorSideIndex()) return true; return false; } protected virtual string GetIPAddressForPlayer(PlayerInfo player) => "0.0.0.0"; /// /// Override this in a derived class to write game lobby specific code to /// spawn.ini. For example, CnCNet game lobbies should write tunnel info /// in this method. /// /// The spawn INI file. protected virtual void WriteSpawnIniAdditions(IniFile iniFile) { // Do nothing by default } private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos) { matchStatistics = new MatchStatistics(ProgramConstants.GAME_VERSION, UniqueGameID, Map.UntranslatedName, GameMode.UntranslatedUIName, Players.Count, GameModeMap.IsCoop); bool isValidForStar = true; foreach (GameLobbyCheckBox checkBox in CheckBoxes) { if (!checkBox.AllowScoring) { isValidForStar = false; break; } } foreach (GameLobbyDropDown dropDown in DropDowns) { if (!dropDown.AllowScoring) { isValidForStar = false; break; } } matchStatistics.IsValidForStar = isValidForStar; for (int pId = 0; pId < Players.Count; pId++) { PlayerInfo pInfo = Players[pId]; matchStatistics.AddPlayer(pInfo.Name, pInfo.Name == ProgramConstants.PLAYERNAME, false, pInfo.SideId == SideCount + RandomSelectorCount, houseInfos[pId].SideIndex + 1, pInfo.TeamId, MPColors.FindIndex(c => c.GameColorIndex == houseInfos[pId].ColorIndex), 10); } for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { var pHouseInfo = houseInfos[Players.Count + aiId]; PlayerInfo aiInfo = AIPlayers[aiId]; matchStatistics.AddPlayer("Computer", false, true, false, pHouseInfo.SideIndex + 1, aiInfo.TeamId, MPColors.FindIndex(c => c.GameColorIndex == pHouseInfo.ColorIndex), aiInfo.AILevel); } } /// /// Writes spawnmap.ini. /// private void WriteMap(PlayerHouseInfo[] houseInfos, Random pseudoRandom) { FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); DeleteSupplementalMapFiles(); spawnMapIniFile.Delete(); Logger.Log("Writing map."); Logger.Log("Loading map INI from " + Map.CompleteFilePath); IniFile mapIni = Map.GetMapIni(); IniFile globalCodeIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Map Code", "GlobalCode.ini")); foreach (IniFile iniFile in GameMode.GetMapRulesIniFiles(pseudoRandom)) MapCodeHelper.ApplyMapCode(mapIni, iniFile); MapCodeHelper.ApplyMapCode(mapIni, globalCodeIni); if (isMultiplayer) { IniFile mpGlobalCodeIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Map Code", "MultiplayerGlobalCode.ini")); MapCodeHelper.ApplyMapCode(mapIni, mpGlobalCodeIni); } else { // Avoid writing the original filename to spawnmap.ini MP games, as it may vary between systems, e.g., when a host uploads a map while other players in game might download it with a diffrent filename. // This inconsistency can result in differing spawnmap.ini files among players, causing desyncs in CnCNet YR games. // Theoretically it can be useful for some singleplayer campaign tracking // But it isn't currently used by any CnCNet game or mod // The code below only applies to the single player case string mapIniFileName = Path.GetFileName(mapIni.FileName); mapIni.SetStringValue("Basic", "OriginalFilename", mapIniFileName); } foreach (GameLobbyCheckBox checkBox in CheckBoxes) checkBox.ApplyMapCode(mapIni, GameMode); foreach (GameLobbyDropDown dropDown in DropDowns) dropDown.ApplyMapCode(mapIni, GameMode); mapIni.MoveSectionToFirst("MultiplayerDialogSettings"); // Required by YR CopySupplementalMapFiles(mapIni); ManipulateStartingLocations(mapIni, houseInfos); mapIni.WriteIniFile(spawnMapIniFile.FullName); } /// /// Some mods require that .map files also have supplemental files copied over with the spawnmap.ini. /// /// This function scans the directory containing the map file and looks for other files with the /// same base filename as the map file that are allowed by the client configuration. /// Those files are then copied to the game base path with the base filename of "spawnmap.EXT". /// /// private void CopySupplementalMapFiles(IniFile mapIni) { var mapFileInfo = new FileInfo(mapIni.FileName); string mapFileBaseName = Path.GetFileNameWithoutExtension(mapFileInfo.Name); IEnumerable supplementalMapFiles = GetSupplementalMapFiles(mapFileInfo.DirectoryName, mapFileBaseName).ToList(); if (!supplementalMapFiles.Any()) return; List supplementalFileNames = new(); foreach (string file in supplementalMapFiles) { try { // Copy each supplemental file string supplementalFileName = $"spawnmap{Path.GetExtension(file)}"; File.Copy(file, SafePath.CombineFilePath(ProgramConstants.GamePath, supplementalFileName), true); supplementalFileNames.Add(supplementalFileName); } catch (Exception ex) { string errorMessage = "Unable to copy supplemental map file".L10N("Client:Main:SupplementalFileCopyError") + $" {file}"; Logger.Log(errorMessage); Logger.Log(ex.ToString()); XNAMessageBox.Show(WindowManager, "Error".L10N("Client:Main:Error"), errorMessage); } } // Write the supplemental map files to the INI (eventual spawnmap.ini) mapIni.SetStringValue("Basic", "SupplementalFiles", string.Join(",", supplementalFileNames)); } /// /// Delete all supplemental map files from last spawn /// private void DeleteSupplementalMapFiles() { IEnumerable supplementalMapFilePaths = GetSupplementalMapFiles(ProgramConstants.GamePath, "spawnmap").ToList(); if (!supplementalMapFilePaths.Any()) return; foreach (string supplementalMapFilename in supplementalMapFilePaths) { try { File.Delete(supplementalMapFilename); } catch (Exception ex) { string errorMessage = "Unable to delete supplemental map file".L10N("Client:Main:SupplementalFileDeleteError") + $" {supplementalMapFilename}"; Logger.Log(errorMessage); Logger.Log(ex.ToString()); XNAMessageBox.Show(WindowManager, "Error".L10N("Client:Main:Error"), errorMessage); } } } private static IEnumerable GetSupplementalMapFiles(string basePath, string baseFileName) { // Get the supplemental file names for allowable extensions var supplementalMapFileNames = ClientConfiguration.Instance.SupplementalMapFileExtensions .Select(ext => $"{baseFileName}.{ext}") .ToList(); if (!supplementalMapFileNames.Any()) return new List(); // Get full file paths for all possible supplemental files return Directory.GetFiles(basePath, $"{baseFileName}.*") .Where(f => supplementalMapFileNames.Contains(Path.GetFileName(f))); } private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] houseInfos) { if (RemoveStartingLocations) { if (GameModeMap.EnforceMaxPlayers) return; // All random starting locations given by the game IniSection waypointSection = mapIni.GetSection("Waypoints"); if (waypointSection == null) return; // TODO implement IniSection.RemoveKey in Rampastring.Tools, then // remove implementation that depends on internal implementation // of IniSection for (int i = 0; i <= 7; i++) { int index = waypointSection.Keys.FindIndex(k => !string.IsNullOrEmpty(k.Key) && k.Key == i.ToString()); if (index > -1) waypointSection.Keys.RemoveAt(index); } } // Multiple players cannot properly share the same starting location // without breaking the SpawnX house logic that pre-placed objects depend on // To work around this, we add new starting locations that just point // to the same cell coordinates as existing stacked starting locations // and make additional players in the same start loc start from the new // starting locations instead. // As an additional restriction, players can only start from waypoints 0 to 7. // That means that if the map already has too many starting waypoints, // we need to move existing (but un-occupied) starting waypoints to point // to the stacked locations so we can spawn the players there. // Check for stacked starting locations (locations with more than 1 player on it) bool[] startingLocationUsed = new bool[MAX_PLAYER_COUNT]; bool stackedStartingLocations = false; foreach (PlayerHouseInfo houseInfo in houseInfos) { if (houseInfo.RealStartingWaypoint > -1) { startingLocationUsed[houseInfo.RealStartingWaypoint] = true; // If assigned starting waypoint is unknown while the real // starting location is known, it means that // the location is shared with another player if (houseInfo.StartingWaypoint == -1) { stackedStartingLocations = true; } } } // If any starting location is stacked, re-arrange all starting locations // so that unused starting locations are removed and made to point at used // starting locations if (!stackedStartingLocations) return; // We also need to modify spawn.ini because WriteSpawnIni // doesn't handle stacked positions. // We could move this code there, but then we'd have to process // the stacked locations in two places (here and in WriteSpawnIni) // because we'd need to modify the map anyway. // Not sure whether having it like this or in WriteSpawnIni // is better, but this implementation is quicker to write for now. IniFile spawnIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS)); // For each player, check if they're sharing the starting location // with someone else // If they are, find an unused waypoint and assign their // starting location to match that for (int pId = 0; pId < houseInfos.Length; pId++) { PlayerHouseInfo houseInfo = houseInfos[pId]; if (houseInfo.RealStartingWaypoint > -1 && houseInfo.StartingWaypoint == -1) { // Find first unused starting location index int unusedLocation = -1; for (int i = 0; i < startingLocationUsed.Length; i++) { if (!startingLocationUsed[i]) { unusedLocation = i; startingLocationUsed[i] = true; break; } } houseInfo.StartingWaypoint = unusedLocation; mapIni.SetIntValue("Waypoints", unusedLocation.ToString(), mapIni.GetIntValue("Waypoints", houseInfo.RealStartingWaypoint.ToString(), 0)); spawnIni.SetIntValue("SpawnLocations", $"Multi{pId + 1}", unusedLocation); } } spawnIni.WriteIniFile(); } /// /// Writes spawn.ini, writes the map file, initializes statistics and /// starts the game process. /// protected virtual void StartGame() { Random pseudoRandom = new Random(RandomSeed); PlayerHouseInfo[] houseInfos = WriteSpawnIni(pseudoRandom); InitializeMatchStatistics(houseInfos); WriteMap(houseInfos, pseudoRandom); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; GameProcessLogic.StartGameProcess(WindowManager); UpdateDiscordPresence(true); } private void GameProcessExited_Callback() => AddCallback(new Action(GameProcessExited), null); protected virtual void GameProcessExited() { GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; Logger.Log("GameProcessExited: Parsing statistics."); matchStatistics?.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false); Logger.Log("GameProcessExited: Adding match to statistics."); StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics); ClearReadyStatuses(); CopyPlayerDataToUI(); UpdateDiscordPresence(true); } /// /// "Copies" player information from the UI to internal memory, /// applying users' player options changes. /// protected virtual void CopyPlayerDataFromUI(object sender, EventArgs e) { if (PlayerUpdatingInProgress) return; var senderDropDown = (XNADropDown)sender; if ((bool)senderDropDown.Tag) ClearReadyStatuses(); var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId; if (Players.Count > MAX_PLAYER_COUNT) throw new Exception($"Player count exceeds maximum of {MAX_PLAYER_COUNT}. How could this happen?"); for (int pId = 0; pId < Players.Count; pId++) { PlayerInfo pInfo = Players[pId]; pInfo.ColorId = ddPlayerColors[pId].SelectedIndex; pInfo.SideId = ddPlayerSides[pId].SelectedIndex; pInfo.StartingLocation = ddPlayerStarts[pId].SelectedIndex; pInfo.TeamId = ddPlayerTeams[pId].SelectedIndex; if (pInfo.SideId == SideCount + RandomSelectorCount) pInfo.StartingLocation = 0; XNADropDown ddName = ddPlayerNames[pId]; switch (ddName.SelectedIndex) { case 0: break; case 1: ddName.SelectedIndex = 0; break; case 2: KickPlayer(pId); break; case 3: BanPlayer(pId); break; } } AIPlayers.Clear(); for (int cmbId = Players.Count; cmbId < MAX_PLAYER_COUNT; cmbId++) { XNADropDown dd = ddPlayerNames[cmbId]; dd.Items[0].Text = "-"; if (dd.SelectedIndex < 1) continue; PlayerInfo aiPlayer = new PlayerInfo { Name = dd.Items[dd.SelectedIndex].Text, AILevel = dd.SelectedIndex - 1, SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0), ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0), StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0), TeamId = Map != null && GameModeMap.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), IsAI = true }; AIPlayers.Add(aiPlayer); } CopyPlayerDataToUI(); btnLaunchGame.SetRank(GetRank()); if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId) UpdateDiscordPresence(); } /// /// Sets the ready status of all non-host human players to false. /// /// If set, players with autoready enabled are reset as well. protected void ClearReadyStatuses(bool resetAutoReady = false) { for (int i = 1; i < Players.Count; i++) { if (resetAutoReady || !Players[i].AutoReady || Players[i].IsInGame) Players[i].Ready = false; } } private bool CanRightClickMultiplayer(XNADropDownItem selectedPlayer) { return selectedPlayer != null && selectedPlayer.Text != ProgramConstants.PLAYERNAME && !ProgramConstants.AI_PLAYER_NAMES.Contains(selectedPlayer.Text); } private void MultiplayerName_RightClick(object sender, EventArgs e) { var selectedPlayer = ((XNADropDown)sender).SelectedItem; if (!CanRightClickMultiplayer(selectedPlayer)) return; if (selectedPlayer == null || selectedPlayer.Text == ProgramConstants.PLAYERNAME) { return; } MultiplayerNameRightClicked?.Invoke(this, new MultiplayerNameRightClickedEventArgs(selectedPlayer.Text)); } /// /// Applies player information changes done in memory to the UI. /// protected virtual void CopyPlayerDataToUI() { PlayerUpdatingInProgress = true; bool allowOptionsChange = AllowPlayerOptionsChange(); var playerExtraOptions = GetPlayerExtraOptions(); if (Players.Count > MAX_PLAYER_COUNT) throw new Exception($"Player count exceeds maximum of {MAX_PLAYER_COUNT}. How could this happen?"); // Human players for (int pId = 0; pId < Players.Count; pId++) { PlayerInfo pInfo = Players[pId]; pInfo.Index = pId; XNADropDown ddPlayerName = ddPlayerNames[pId]; ddPlayerName.Items[0].Text = pInfo.Name; ddPlayerName.Items[1].Text = string.Empty; ddPlayerName.Items[2].Text = "Kick".L10N("Client:Main:Kick"); ddPlayerName.Items[3].Text = "Ban".L10N("Client:Main:Ban"); ddPlayerName.SelectedIndex = 0; ddPlayerName.AllowDropDown = false; bool allowPlayerOptionsChange = allowOptionsChange || pInfo.Name == ProgramConstants.PLAYERNAME; ddPlayerSides[pId].SelectedIndex = pInfo.SideId; ddPlayerSides[pId].AllowDropDown = !playerExtraOptions.IsForceRandomSides && allowPlayerOptionsChange; ddPlayerColors[pId].SelectedIndex = pInfo.ColorId; ddPlayerColors[pId].AllowDropDown = !playerExtraOptions.IsForceRandomColors && allowPlayerOptionsChange; ddPlayerStarts[pId].SelectedIndex = pInfo.StartingLocation; ddPlayerTeams[pId].SelectedIndex = pInfo.TeamId; if (GameModeMap != null) { ddPlayerTeams[pId].AllowDropDown = !playerExtraOptions.IsForceNoTeams && allowPlayerOptionsChange && !GameModeMap.IsCoop && !GameModeMap.ForceNoTeams; ddPlayerStarts[pId].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowPlayerOptionsChange && !GameModeMap.ForceRandomStartLocations; } } // AI players for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { PlayerInfo aiInfo = AIPlayers[aiId]; int index = Players.Count + aiId; aiInfo.Index = index; XNADropDown ddPlayerName = ddPlayerNames[index]; ddPlayerName.Items[0].Text = "-"; ddPlayerName.Items[1].Text = ProgramConstants.AI_PLAYER_NAMES[0]; ddPlayerName.Items[2].Text = ProgramConstants.AI_PLAYER_NAMES[1]; ddPlayerName.Items[3].Text = ProgramConstants.AI_PLAYER_NAMES[2]; ddPlayerName.SelectedIndex = 1 + aiInfo.AILevel; ddPlayerName.AllowDropDown = allowOptionsChange; ddPlayerSides[index].SelectedIndex = aiInfo.SideId; ddPlayerSides[index].AllowDropDown = !playerExtraOptions.IsForceRandomSides && allowOptionsChange; ddPlayerColors[index].SelectedIndex = aiInfo.ColorId; ddPlayerColors[index].AllowDropDown = !playerExtraOptions.IsForceRandomColors && allowOptionsChange; ddPlayerStarts[index].SelectedIndex = aiInfo.StartingLocation; ddPlayerTeams[index].SelectedIndex = aiInfo.TeamId; if (GameModeMap != null) { ddPlayerTeams[index].AllowDropDown = !playerExtraOptions.IsForceNoTeams && allowOptionsChange && !GameModeMap.IsCoop && !GameModeMap.ForceNoTeams; ddPlayerStarts[index].AllowDropDown = !playerExtraOptions.IsForceRandomStarts && allowOptionsChange && !GameModeMap.ForceRandomStartLocations; } } // Unused player slots for (int ddIndex = Players.Count + AIPlayers.Count; ddIndex < MAX_PLAYER_COUNT; ddIndex++) { XNADropDown ddPlayerName = ddPlayerNames[ddIndex]; ddPlayerName.AllowDropDown = false; ddPlayerName.Items[0].Text = string.Empty; ddPlayerName.Items[1].Text = ProgramConstants.AI_PLAYER_NAMES[0]; ddPlayerName.Items[2].Text = ProgramConstants.AI_PLAYER_NAMES[1]; ddPlayerName.Items[3].Text = ProgramConstants.AI_PLAYER_NAMES[2]; ddPlayerName.SelectedIndex = 0; ddPlayerSides[ddIndex].SelectedIndex = -1; ddPlayerSides[ddIndex].AllowDropDown = false; ddPlayerColors[ddIndex].SelectedIndex = -1; ddPlayerColors[ddIndex].AllowDropDown = false; ddPlayerStarts[ddIndex].SelectedIndex = -1; ddPlayerStarts[ddIndex].AllowDropDown = false; ddPlayerTeams[ddIndex].SelectedIndex = -1; ddPlayerTeams[ddIndex].AllowDropDown = false; } if (allowOptionsChange && Players.Count + AIPlayers.Count < MAX_PLAYER_COUNT) ddPlayerNames[Players.Count + AIPlayers.Count].AllowDropDown = true; MapPreviewBox.UpdateStartingLocationTexts(); UpdateMapPreviewBoxEnabledStatus(); CheckDisallowedSides(); PlayerUpdatingInProgress = false; } /// /// Updates the enabled status of starting location selectors /// in the map preview box. /// protected abstract void UpdateMapPreviewBoxEnabledStatus(); /// /// Override this in a derived class to kick players. /// /// The index of the player that should be kicked. protected virtual void KickPlayer(int playerIndex) { // Do nothing by default } /// /// Override this in a derived class to ban players. /// /// The index of the player that should be banned. protected virtual void BanPlayer(int playerIndex) { // Do nothing by default } /// /// Updates the map information labels such as name and author. /// protected virtual void SetMapLabels() { if (GameMode == null || Map == null) { lblMapName.Text = "Map: Unknown".L10N("Client:Main:MapUnknown"); lblMapAuthor.Text = "By Unknown Author".L10N("Client:Main:AuthorByUnknown"); lblGameMode.Text = "Game mode: Unknown".L10N("Client:Main:GameModeUnknown"); lblMapSize.Text = "Size: Not available".L10N("Client:Main:MapSizeUnknown"); return; } lblMapName.Text = "Map:".L10N("Client:Main:Map") + " " + Renderer.GetSafeString(Map.Name, lblMapName.FontIndex); lblMapAuthor.Text = "By".L10N("Client:Main:AuthorBy") + " " + Renderer.GetSafeString(Map.Author, lblMapAuthor.FontIndex); lblGameMode.Text = "Game mode:".L10N("Client:Main:GameModeLabel") + " " + GameMode.UIName; lblMapSize.Text = "Size:".L10N("Client:Main:MapSize") + " " + Map.GetSizeString(); } /// /// Changes the current map and game mode. /// /// The new game mode map. protected virtual void ChangeMap(GameModeMap gameModeMap) { GameModeMap = gameModeMap; _ = UpdateLaunchGameButtonStatus(); SetMapLabels(); if (GameMode == null || Map == null) { MapPreviewBox.GameModeMap = null; OnGameOptionChanged(); return; } disableGameOptionUpdateBroadcast = true; // Clear forced options foreach (var ddGameOption in DropDowns) ddGameOption.AllowDropDown = true; foreach (var checkBox in CheckBoxes) checkBox.AllowChecking = true; // We could either pass the CheckBoxes and DropDowns of this class // to the Map and GameMode instances and let them apply their forced // options, or we could do it in this class with helper functions. // The second approach is probably clearer. // We use these temp lists to determine which options WERE NOT forced // by the map. We then return these to user-defined settings. // This prevents forced options from one map getting carried // to other maps. var checkBoxListClone = new List(CheckBoxes); var dropDownListClone = new List(DropDowns); ApplyForcedCheckBoxOptions(checkBoxListClone, GameMode.ForcedCheckBoxValues); ApplyForcedCheckBoxOptions(checkBoxListClone, Map.ForcedCheckBoxValues); ApplyForcedDropDownOptions(dropDownListClone, GameMode.ForcedDropDownValues); ApplyForcedDropDownOptions(dropDownListClone, Map.ForcedDropDownValues); foreach (var chkBox in checkBoxListClone) chkBox.Checked = chkBox.HostChecked; foreach (var dd in dropDownListClone) dd.SelectedIndex = dd.HostSelectedIndex; // Enable all sides by default foreach (var ddSide in ddPlayerSides) { ddSide.Items.ForEach(item => item.Selectable = true); } // Enable all colors by default foreach (var ddColor in ddPlayerColors) { for (int i = 0; i < ddColor.Items.Count; i++) { ddColor.Items[i].Selectable = true; ddColor.SetItemColorEnabled(i, true); } } // Apply starting locations foreach (var ddStart in ddPlayerStarts) { ddStart.Items.Clear(); ddStart.AddItem("???"); int maxLocation = GameModeMap.MaxPlayers == 0 ? 0 : (GameModeMap.AllowedStartingLocations.Max() == GameModeMap.MaxPlayers ? GameModeMap.MaxPlayers : MAX_PLAYER_COUNT); for (int i = 1; i <= maxLocation; i++) { if (GameModeMap.AllowedStartingLocations.Contains(i)) ddStart.AddItem(i.ToString()); else ddStart.AddItem(new XNADropDownItem() { Text = i.ToString(), Selectable = false }); } } // Check if AI players allowed bool AIAllowed = !GameModeMap.HumanPlayersOnly; foreach (var ddName in ddPlayerNames) { if (ddName.Items.Count > 3) { ddName.Items[1].Selectable = AIAllowed; ddName.Items[2].Selectable = AIAllowed; ddName.Items[3].Selectable = AIAllowed; } } if (!AIAllowed) AIPlayers.Clear(); IEnumerable concatPlayerList = Players.Concat(AIPlayers).ToList(); foreach (PlayerInfo pInfo in concatPlayerList) { if (!GameModeMap.AllowedStartingLocations.Contains(pInfo.StartingLocation) || GameModeMap.ForceRandomStartLocations) pInfo.StartingLocation = 0; if (!GameModeMap.IsCoop && GameModeMap.ForceNoTeams) pInfo.TeamId = 0; } if (GameModeMap.CoopInfo != null) { // Co-Op map disallowed color logic foreach (int disallowedColorIndex in GameModeMap.CoopInfo.DisallowedPlayerColors) { if (disallowedColorIndex >= MPColors.Count) continue; foreach (var ddColor in ddPlayerColors) { ddColor.Items[disallowedColorIndex + 1].Selectable = false; ddColor.SetItemColorEnabled(disallowedColorIndex + 1, false); } foreach (PlayerInfo pInfo in concatPlayerList) { if (pInfo.ColorId == disallowedColorIndex + 1) pInfo.ColorId = 0; } } // Force teams foreach (PlayerInfo pInfo in concatPlayerList) pInfo.TeamId = 1; if (PlayerExtraOptionsPanel != null) { PlayerExtraOptionsPanel.ForcedNoTeamsAllowChecking = false; PlayerExtraOptionsPanel.ForcedNoTeams = false; PlayerExtraOptionsPanel.UseTeamStartMappingsAllowChecking = false; PlayerExtraOptionsPanel.UseTeamStartMappings = false; } } else { if (PlayerExtraOptionsPanel != null) { PlayerExtraOptionsPanel.ForcedNoTeamsAllowChecking = true; PlayerExtraOptionsPanel.UseTeamStartMappingsAllowChecking = true; } } OnGameOptionChanged(); MapPreviewBox.GameModeMap = GameModeMap; CopyPlayerDataToUI(); disableGameOptionUpdateBroadcast = false; PlayerExtraOptionsPanel?.UpdateForGameModeMap(GameModeMap); } private void ApplyForcedCheckBoxOptions(List optionList, List> forcedOptions) { foreach (KeyValuePair option in forcedOptions) { GameLobbyCheckBox checkBox = CheckBoxes.Find(chk => chk.Name == option.Key); if (checkBox != null) { checkBox.Checked = option.Value; checkBox.AllowChecking = false; optionList.Remove(checkBox); } } } private void ApplyForcedDropDownOptions(List optionList, List> forcedOptions) { foreach (KeyValuePair option in forcedOptions) { GameLobbyDropDown dropDown = DropDowns.Find(dd => dd.Name == option.Key); if (dropDown != null) { dropDown.SelectedIndex = option.Value; dropDown.AllowDropDown = false; optionList.Remove(dropDown); } } } protected string AILevelToName(int aiLevel) { return ProgramConstants.GetAILevelName(aiLevel); } protected GameType GetGameType() { int teamCount = GetPvPTeamCount(); if (teamCount == 0) return GameType.FFA; if (teamCount == 1) return GameType.Coop; return GameType.TeamGame; } protected Rank GetRank() { if (GameMode == null || Map == null) return Rank.None; foreach (GameLobbyCheckBox checkBox in CheckBoxes) { if (checkBox.AllowScoring) return Rank.None; } foreach (GameLobbyDropDown dropDown in DropDowns) { if (dropDown.AllowScoring) return Rank.None; } PlayerInfo localPlayer = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (localPlayer == null) return Rank.None; if (IsPlayerSpectator(localPlayer)) return Rank.None; // These variables are used by both the skirmish and multiplayer code paths int[] teamMemberCounts = new int[5]; int lowestEnemyAILevel = 2; int highestAllyAILevel = 0; foreach (PlayerInfo aiPlayer in AIPlayers) { teamMemberCounts[aiPlayer.TeamId]++; if (aiPlayer.TeamId > 0 && aiPlayer.TeamId == localPlayer.TeamId) { if (aiPlayer.AILevel > highestAllyAILevel) highestAllyAILevel = aiPlayer.AILevel; } else { if (aiPlayer.AILevel < lowestEnemyAILevel) lowestEnemyAILevel = aiPlayer.AILevel; } } if (isMultiplayer) { if (Players.Count == 1) return Rank.None; // PvP stars for 2-player and 3-player maps if (GameModeMap.MaxPlayers <= 3) { List filteredPlayers = Players.Where(p => !IsPlayerSpectator(p)).ToList(); if (AIPlayers.Count > 0) return Rank.None; if (filteredPlayers.Count != GameModeMap.MaxPlayers) return Rank.None; int localTeamIndex = localPlayer.TeamId; if (localTeamIndex > 0 && filteredPlayers.Count(p => p.TeamId == localTeamIndex) > 1) return Rank.None; return Rank.Hard; } // Coop stars for maps with 4 or more players // See the code in StatisticsManager.GetRankForCoopMatch for the conditions if (Players.Find(p => IsPlayerSpectator(p)) != null) return Rank.None; if (AIPlayers.Count == 0) return Rank.None; if (Players.Find(p => p.TeamId != localPlayer.TeamId) != null) return Rank.None; if (Players.Find(p => p.TeamId == 0) != null) return Rank.None; if (AIPlayers.Find(p => p.TeamId == 0) != null) return Rank.None; teamMemberCounts[localPlayer.TeamId] += Players.Count; if (lowestEnemyAILevel < highestAllyAILevel) { // Check that the player's AI allies aren't stronger return Rank.None; } // Check that all teams have at least as many players // as the human players' team int allyCount = teamMemberCounts[localPlayer.TeamId]; for (int i = 1; i < 5; i++) { if (i == localPlayer.TeamId) continue; if (teamMemberCounts[i] > 0) { if (teamMemberCounts[i] < allyCount) return Rank.None; } } return lowestEnemyAILevel + 1; } // ********* // Skirmish! // ********* if (AIPlayers.Count != GameModeMap.MaxPlayers - 1) return Rank.None; teamMemberCounts[localPlayer.TeamId]++; if (lowestEnemyAILevel < highestAllyAILevel) { // Check that the player's AI allies aren't stronger return Rank.None; } if (localPlayer.TeamId > 0) { // Check that all teams have at least as many players // as the local player's team int allyCount = teamMemberCounts[localPlayer.TeamId]; for (int i = 1; i < 5; i++) { if (i == localPlayer.TeamId) continue; if (teamMemberCounts[i] > 0) { if (teamMemberCounts[i] < allyCount) return Rank.None; } } // Check that there is a team other than the players' team that is at least as large bool pass = false; for (int i = 1; i < 5; i++) { if (i == localPlayer.TeamId) continue; if (teamMemberCounts[i] >= allyCount) { pass = true; break; } } if (!pass) return Rank.None; } return lowestEnemyAILevel + 1; } protected string AddGameOptionPreset(string name) { string error = GameOptionPreset.IsNameValid(name); if (!string.IsNullOrEmpty(error)) return error; GameOptionPreset preset = new GameOptionPreset(name); foreach (GameLobbyCheckBox checkBox in CheckBoxes) { preset.AddCheckBoxValue(checkBox.Name, checkBox.Checked); } foreach (GameLobbyDropDown dropDown in DropDowns) { preset.AddDropDownValue(dropDown.Name, dropDown.SelectedIndex); } GameOptionPresets.Instance.AddPreset(preset); return null; } public bool LoadGameOptionPreset(string name) { GameOptionPreset preset = GameOptionPresets.Instance.GetPreset(name); if (preset == null) return false; disableGameOptionUpdateBroadcast = true; var checkBoxValues = preset.GetCheckBoxValues(); foreach (var kvp in checkBoxValues) { GameLobbyCheckBox checkBox = CheckBoxes.Find(c => c.Name == kvp.Key); if (checkBox != null && checkBox.AllowChanges && checkBox.AllowChecking) { checkBox.Checked = kvp.Value; checkBox.HostChecked = kvp.Value; } } var dropDownValues = preset.GetDropDownValues(); foreach (var kvp in dropDownValues) { GameLobbyDropDown dropDown = DropDowns.Find(d => d.Name == kvp.Key); if (dropDown != null && dropDown.AllowDropDown) { dropDown.SelectedIndex = kvp.Value; dropDown.HostSelectedIndex = kvp.Value; } } disableGameOptionUpdateBroadcast = false; OnGameOptionChanged(); return true; } /// /// Checks if launch game button can stay enabled or not and updates the state accordingly. /// /// True if launch game button is enabled, false if not. protected virtual bool UpdateLaunchGameButtonStatus() { return true; } protected abstract bool AllowPlayerOptionsChange(); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyCheckBox.cs ================================================ using System.Collections.Generic; using System.Linq; using ClientCore.Extensions; using DTAClient.DXGUI.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer.GameLobby; public class GameLobbyCheckBox : GameSessionCheckBox { public GameLobbyCheckBox(WindowManager windowManager) : base(windowManager) { } public bool IsMultiplayer { get; set; } /// /// The last host-defined value for this check box. /// Defaults to the default value of Checked after the check-box /// has been initialized, but its value is only changed by user interaction. /// public bool HostChecked { get; set; } /// /// The last value that the local player gave for this check box. /// Defaults to the default value of Checked after the check-box /// has been initialized, but its value is only changed by user interaction. /// public bool UserChecked { get; set; } /// /// The side indices that this check box disallows when checked. /// Defaults to -1, which means none. /// public List DisallowedSideIndices = new(); public override void Initialize() { // Find the game lobby that this control belongs to and register ourselves as a game option. XNAControl parent = Parent; while (true) { if (parent == null) break; // oh no, we have a circular class reference here! if (parent is GameLobbyBase configView) { configView.CheckBoxes.Add(this); break; } parent = parent.Parent; } base.Initialize(); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { switch (key) { case "CheckedMP": if (IsMultiplayer) Checked = Conversions.BooleanFromString(value, false); return; case "Checked": bool checkedValue = Conversions.BooleanFromString(value, false); HostChecked = checkedValue; UserChecked = checkedValue; break; // let base method handle it too as we're not replacing it fully case "DisallowedSideIndex": case "DisallowedSideIndices": List sides = value.SplitWithCleanup() .Select(s => Conversions.IntFromString(s, -1)) .Distinct() .ToList(); DisallowedSideIndices.AddRange(sides.Where(s => !DisallowedSideIndices.Contains(s))); return; } base.ParseControlINIAttribute(iniFile, key, value); } /// /// Applies the check-box's disallowed side index to a bool /// array that determines which sides are disabled. /// /// An array that determines which sides are disabled. public void ApplyDisallowedSideIndex(bool[] disallowedArray) { if (DisallowedSideIndices == null || DisallowedSideIndices.Count == 0) return; if (Checked != reversed) { for (int i = 0; i < DisallowedSideIndices.Count; i++) { int sideNotAllowed = DisallowedSideIndices[i]; disallowedArray[sideNotAllowed] = true; } } } public override void OnLeftClick(InputEventArgs inputEventArgs) { // FIXME there's a discrepancy with how base XNAUI handles this // it doesn't set handled if changing the setting is not allowed inputEventArgs.Handled = true; if (!AllowChanges) return; base.OnLeftClick(inputEventArgs); UserChecked = Checked; } public override void Draw(GameTime gameTime) { if (ShowIconInGameLobby) { string iconName = Checked ? EnabledIcon : DisabledIcon; if (!string.IsNullOrEmpty(iconName)) { Texture2D icon = AssetLoader.LoadTexture(iconName); if (icon != null) { const int iconSpacing = 6; int iconX = -icon.Width - iconSpacing; int iconY = (Height - icon.Height) / 2; DrawTexture(icon, new Rectangle(iconX, iconY, icon.Width, icon.Height), Color.White); } } } base.Draw(gameTime); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs ================================================ using DTAClient.DXGUI.Generic; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer.GameLobby; public class GameLobbyDropDown : GameSessionDropDown { public GameLobbyDropDown(WindowManager windowManager) : base(windowManager) { } public int HostSelectedIndex { get; set; } public int UserSelectedIndex { get; set; } public override void Initialize() { // Find the game lobby that this control belongs to and register ourselves as a game option. XNAControl parent = Parent; while (true) { if (parent == null) break; // oh no, we have a circular class reference here! if (parent is GameLobbyBase configView) { configView.DropDowns.Add(this); break; } parent = parent.Parent; } base.Initialize(); } protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value) { if (key == "DefaultIndex") { int index = int.Parse(value); HostSelectedIndex = index; UserSelectedIndex = index; // don't return, let base method handle it's part too } base.ParseControlINIAttribute(iniFile, key, value); } public override void OnLeftClick(InputEventArgs inputEventArgs) { // FIXME there's a discrepancy with how base XNAUI handles this // it doesn't set handled if changing the setting is not allowed inputEventArgs.Handled = true; if (!AllowDropDown) return; base.OnLeftClick(inputEventArgs); UserSelectedIndex = SelectedIndex; } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbySettingsEventArgs.cs ================================================ using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby; public class GameLobbySettingsEventArgs(string gameRoomName, int maxPlayers, int skillLevel, string password) : EventArgs { public string GameRoomName { get; } = gameRoomName; public int MaxPlayers { get; } = maxPlayers; public int SkillLevel { get; } = skillLevel; public string Password { get; } = password; } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbySettingsWindow.cs ================================================ using ClientCore; using ClientCore.Extensions; using ClientGUI; using DTAClient.Domain.Multiplayer.CnCNet; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; namespace DTAClient.DXGUI.Multiplayer.GameLobby; /// /// A window that allows the host to modify game lobby settings. /// public class GameLobbySettingsWindow(WindowManager windowManager) : XNAWindow(windowManager) { public event EventHandler SettingsChanged; private XNATextBox tbGameName; private XNATextBox tbPassword; private XNAClientDropDown ddMaxPlayers; private XNAClientDropDown ddSkillLevel; private XNALabel lblRoomName; private XNALabel lblPassword; private XNALabel lblMaxPlayers; private XNALabel lblSkillLevel; private XNAClientButton btnSave; private XNAClientButton btnCancel; private string[] SkillLevelOptions; public override void Initialize() { SkillLevelOptions = ClientConfiguration.Instance.SkillLevelOptions.Split(','); Name = "GameLobbySettingsWindow"; ClientRectangle = new Rectangle(0, 0, 400, 240); BackgroundTexture = AssetLoader.LoadTexture("gamecreationoptionsbg.png"); lblRoomName = new XNALabel(WindowManager); lblRoomName.Name = nameof(lblRoomName); lblRoomName.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, UIDesignConstants.EMPTY_SPACE_TOP + UIDesignConstants.CONTROL_VERTICAL_MARGIN, 0, 0); lblRoomName.Text = "Game room name:".L10N("Client:Main:GameRoomName"); tbGameName = new XNATextBox(WindowManager); tbGameName.Name = nameof(tbGameName); tbGameName.MaximumTextLength = 23; tbGameName.ClientRectangle = new Rectangle(Width - 200 - UIDesignConstants.EMPTY_SPACE_SIDES - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, lblRoomName.Y - 2, 200, 21); int nextY = tbGameName.Bottom + 15; lblPassword = new XNALabel(WindowManager); lblPassword.Name = nameof(lblPassword); lblPassword.ClientRectangle = new Rectangle(lblRoomName.X, nextY, 0, 0); lblPassword.Text = "Password:".L10N("Client:Main:LobbyPassword"); tbPassword = new XNATextBox(WindowManager); tbPassword.Name = nameof(tbPassword); tbPassword.MaximumTextLength = 20; tbPassword.ClientRectangle = new Rectangle(tbGameName.X, lblPassword.Y - 2, 200, 21); nextY = tbPassword.Bottom + 15; lblMaxPlayers = new XNALabel(WindowManager); lblMaxPlayers.Name = nameof(lblMaxPlayers); lblMaxPlayers.ClientRectangle = new Rectangle(lblRoomName.X, nextY, 0, 0); lblMaxPlayers.Text = "Max players:".L10N("Client:Main:GameMaxPlayers"); ddMaxPlayers = new XNAClientDropDown(WindowManager); ddMaxPlayers.Name = nameof(ddMaxPlayers); ddMaxPlayers.ClientRectangle = new Rectangle(tbGameName.X, lblMaxPlayers.Y - 2, tbGameName.Width, 21); for (int i = 8; i > 1; i--) ddMaxPlayers.AddItem(i.ToString()); ddMaxPlayers.SelectedIndex = 0; nextY = ddMaxPlayers.Bottom + 15; lblSkillLevel = new XNALabel(WindowManager); lblSkillLevel.Name = nameof(lblSkillLevel); lblSkillLevel.ClientRectangle = new Rectangle(lblRoomName.X, nextY, 0, 0); lblSkillLevel.Text = "Preferred skill level:".L10N("Client:Main:PreferredSkillLevel"); ddSkillLevel = new XNAClientDropDown(WindowManager); ddSkillLevel.Name = nameof(ddSkillLevel); ddSkillLevel.ClientRectangle = new Rectangle(tbGameName.X, lblSkillLevel.Y - 2, tbGameName.Width, 21); for (int i = 0; i < SkillLevelOptions.Length; i++) { string skillLevel = SkillLevelOptions[i]; string localizedSkillLevel = skillLevel.L10N($"INI:ClientDefinitions:SkillLevel:{i}"); ddSkillLevel.AddItem(localizedSkillLevel); } ddSkillLevel.SelectedIndex = ClientConfiguration.Instance.DefaultSkillLevelIndex; nextY = ddSkillLevel.Bottom + 20; btnSave = new XNAClientButton(WindowManager); btnSave.Name = nameof(btnSave); btnSave.ClientRectangle = new Rectangle(UIDesignConstants.EMPTY_SPACE_SIDES + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, nextY, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnSave.Text = "Save".L10N("Client:Main:ButtonSave"); btnSave.LeftClick += BtnSave_LeftClick; btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = nameof(btnCancel); btnCancel.ClientRectangle = new Rectangle(Width - UIDesignConstants.BUTTON_WIDTH_133 - UIDesignConstants.EMPTY_SPACE_SIDES - UIDesignConstants.CONTROL_HORIZONTAL_MARGIN, btnSave.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(lblRoomName); AddChild(tbGameName); AddChild(lblPassword); AddChild(tbPassword); AddChild(lblMaxPlayers); AddChild(ddMaxPlayers); AddChild(lblSkillLevel); AddChild(ddSkillLevel); AddChild(btnSave); AddChild(btnCancel); Height = btnSave.Bottom + UIDesignConstants.CONTROL_VERTICAL_MARGIN + UIDesignConstants.EMPTY_SPACE_BOTTOM; base.Initialize(); CenterOnParent(); } public void Open(string currentGameName, int currentMaxPlayers, int currentSkillLevel, string currentPassword) { tbGameName.Text = currentGameName; tbPassword.Text = currentPassword ?? string.Empty; ddMaxPlayers.SelectedIndex = 8 - currentMaxPlayers; ddSkillLevel.SelectedIndex = currentSkillLevel; Enable(); } private void BtnSave_LeftClick(object sender, EventArgs e) { string gameName = NameValidator.GetSanitizedGameName(tbGameName.Text); NameValidationError validationError = NameValidator.IsGameNameValid(gameName, out string errorMessage); if (validationError != NameValidationError.None) { XNAMessageBox.Show(WindowManager, "Invalid game name".L10N("Client:Main:InvalidGameName"), errorMessage); return; } int maxPlayers = int.Parse(ddMaxPlayers.SelectedItem.Text); int skillLevel = ddSkillLevel.SelectedIndex; string password = tbPassword.Text; SettingsChanged?.Invoke(this, new GameLobbySettingsEventArgs( gameName, maxPlayers, skillLevel, password)); Disable(); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Disable(); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameModeMapFilter.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using DTAClient.Domain.Multiplayer; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class GameModeMapFilter { public Func> GetGameModeMaps; public GameModeMapFilter(Func> filterAction) { GetGameModeMaps = filterAction; } public bool Any() => GetGameModeMaps().Any(); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/GameType.cs ================================================ namespace DTAClient.DXGUI.Multiplayer.GameLobby { public enum GameType { Undefined, FFA, TeamGame, Coop } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs ================================================ using ClientCore; using DTAClient.Domain; using DTAClient.Domain.LAN; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.LAN; using DTAClient.DXGUI.Generic; using DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers; using DTAClient.Online; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using DTAClient.DXGUI.Multiplayer.CnCNet; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class LANGameLobby : MultiplayerGameLobby { private const int GAME_OPTION_SPECIAL_FLAG_COUNT = 5; private const double DROPOUT_TIMEOUT = 20.0; private const double GAME_BROADCAST_INTERVAL = 2.0; private const string CHAT_COMMAND = "GLCHAT"; private const string RETURN_COMMAND = "RETURN"; private const string GET_READY_COMMAND = "GETREADY"; private const string PLAYER_OPTIONS_REQUEST_COMMAND = "POREQ"; private const string PLAYER_OPTIONS_BROADCAST_COMMAND = "POPTS"; private const string PLAYER_JOIN_COMMAND = "JOIN"; private const string PLAYER_QUIT_COMMAND = "QUIT"; private const string GAME_OPTIONS_COMMAND = "OPTS"; private const string PLAYER_READY_REQUEST = "READY"; private const string LAUNCH_GAME_COMMAND = "LAUNCH"; private const string FILE_HASH_COMMAND = "FHASH"; private const string DICE_ROLL_COMMAND = "DR"; public const string PING = "PING"; public LANGameLobby(WindowManager windowManager, string iniName, TopBar topBar, LANColor[] chatColors, MapLoader mapLoader, DiscordHandler discordHandler, PrivateMessagingWindow pmWindow, Random random) : base(windowManager, iniName, topBar, mapLoader, discordHandler, pmWindow, random) { this.chatColors = chatColors; encoding = Encoding.UTF8; hostCommandHandlers = new CommandHandlerBase[] { new StringCommandHandler(CHAT_COMMAND, GameHost_HandleChatCommand), new NoParamCommandHandler(RETURN_COMMAND, GameHost_HandleReturnCommand), new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, HandlePlayerOptionsRequest), new NoParamCommandHandler(PLAYER_QUIT_COMMAND, HandlePlayerQuit), new StringCommandHandler(PLAYER_READY_REQUEST, GameHost_HandleReadyRequest), new StringCommandHandler(FILE_HASH_COMMAND, HandleFileHashCommand), new StringCommandHandler(DICE_ROLL_COMMAND, Host_HandleDiceRoll), new NoParamCommandHandler(PING, s => { }), }; playerCommandHandlers = new LANClientCommandHandler[] { new ClientStringCommandHandler(CHAT_COMMAND, Player_HandleChatCommand), new ClientNoParamCommandHandler(GET_READY_COMMAND, HandleGetReadyCommand), new ClientNoParamCommandHandler(PLAYER_QUIT_COMMAND, HandleHostQuit), new ClientStringCommandHandler(RETURN_COMMAND, Player_HandleReturnCommand), new ClientStringCommandHandler(PLAYER_OPTIONS_BROADCAST_COMMAND, HandlePlayerOptionsBroadcast), new ClientStringCommandHandler(PlayerExtraOptions.LAN_MESSAGE_KEY, HandlePlayerExtraOptionsBroadcast), new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, HandleGameLaunchCommand), new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, HandleGameOptionsMessage), new ClientStringCommandHandler(DICE_ROLL_COMMAND, Client_HandleDiceRoll), new ClientNoParamCommandHandler(PING, HandlePing), }; localGame = ClientConfiguration.Instance.LocalGame; WindowManager.GameClosing += WindowManager_GameClosing; this.random = random; } private void WindowManager_GameClosing(object sender, EventArgs e) { if (client != null && client.Connected) Clear(); } private void HandleFileHashCommand(string sender, string fileHash) { if (fileHash != localFileHash) AddNotice(string.Format("{0} has modified game files! They could be cheating!".L10N("Client:Main:PlayerModifiedFiles"), sender)); PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; pInfo.HashReceived = true; CopyPlayerDataToUI(); } public event EventHandler LobbyNotification; public event EventHandler GameLeft; public event EventHandler GameBroadcast; private TcpListener listener; private TcpClient client; private volatile bool leaving; private int sessionId; private IPEndPoint hostEndPoint; private LANColor[] chatColors; private int chatColorIndex; private Encoding encoding; private CommandHandlerBase[] hostCommandHandlers; private LANClientCommandHandler[] playerCommandHandlers; private TimeSpan timeSinceGameBroadcast = TimeSpan.Zero; private TimeSpan timeSinceLastReceivedCommand = TimeSpan.Zero; private string overMessage = string.Empty; private string localGame; private string localFileHash; private Random random; public override void Initialize() { IniNameOverride = nameof(LANGameLobby); base.Initialize(); PostInitialize(); } public void SetUp(bool isHost, IPEndPoint hostEndPoint, TcpClient client) { leaving = false; sessionId++; Refresh(isHost); this.hostEndPoint = hostEndPoint; if (isHost) { RandomSeed = random.Next(); Thread thread = new Thread(ListenForClients); thread.Start(); this.client = new TcpClient(); this.client.Connect("127.0.0.1", ProgramConstants.LAN_GAME_LOBBY_PORT); byte[] buffer = encoding.GetBytes(PLAYER_JOIN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME); this.client.GetStream().Write(buffer, 0, buffer.Length); this.client.GetStream().Flush(); var fhc = new FileHashCalculator(); fhc.CalculateHashes(); localFileHash = fhc.GetCompleteHash(); RefreshMapSelectionUI(); } else { this.client = client; } new Thread(HandleServerCommunication).Start(); if (IsHost) CopyPlayerDataToUI(); WindowManager.SelectedControl = tbChatInput; } public void PostJoin() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(); SendMessageToHost(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash()); ResetAutoReadyCheckbox(); } #region Server code private void ListenForClients() { listener = new TcpListener(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); listener.Start(); while (true) { TcpClient client; try { client = listener.AcceptTcpClient(); } catch (Exception ex) { Logger.Log("Listener error: " + ex.ToString()); break; } Logger.Log("New client connected from " + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString()); if (Players.Count >= MAX_PLAYER_COUNT) { Logger.Log("Dropping client because of player limit."); client.Close(); continue; } if (Locked) { Logger.Log("Dropping client because the game room is locked."); client.Close(); continue; } LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); lpInfo.SetClient(client); Thread thread = new Thread(new ParameterizedThreadStart(HandleClientConnection)); thread.Start(lpInfo); } } private void HandleClientConnection(object clientInfo) { var lpInfo = (LANPlayerInfo)clientInfo; byte[] message = new byte[1024]; while (true) { int bytesRead = 0; try { bytesRead = lpInfo.TcpClient.GetStream().Read(message, 0, message.Length); } catch (Exception ex) { Logger.Log("Socket error with client " + lpInfo.IPAddress + "; removing. Message: " + ex.ToString()); break; } if (bytesRead == 0) { Logger.Log("Connect attempt from " + lpInfo.IPAddress + " failed! (0 bytes read)"); break; } string msg = encoding.GetString(message, 0, bytesRead); string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length != 2) break; string name = parts[1].Trim(); if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name)) { lpInfo.Name = name; AddCallback(new Action(AddPlayer), lpInfo); return; } break; } if (lpInfo.TcpClient.Connected) lpInfo.TcpClient.Close(); } private void AddPlayer(LANPlayerInfo lpInfo) { if (Players.Find(p => p.Name == lpInfo.Name) != null || Players.Count >= MAX_PLAYER_COUNT || Locked) return; Players.Add(lpInfo); if (IsHost && Players.Count == 1) Players[0].Ready = true; lpInfo.MessageReceived += LpInfo_MessageReceived; lpInfo.ConnectionLost += LpInfo_ConnectionLost; AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); lpInfo.StartReceiveLoop(); CopyPlayerDataToUI(); BroadcastPlayerOptions(); BroadcastPlayerExtraOptions(); OnGameOptionChanged(); UpdateDiscordPresence(); } private void LpInfo_ConnectionLost(object sender, EventArgs e) { AddCallback(new Action(HandleConnectionLost), (LANPlayerInfo)sender); } private void HandleConnectionLost(LANPlayerInfo lpInfo) { CleanUpPlayer(lpInfo); Players.Remove(lpInfo); AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); CopyPlayerDataToUI(); BroadcastPlayerOptions(); if (lpInfo.Name == ProgramConstants.PLAYERNAME) ResetDiscordPresence(); else UpdateDiscordPresence(); } private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e) { AddCallback(new Action(HandleClientMessage), e.Message, (LANPlayerInfo)sender); } private void HandleClientMessage(string data, LANPlayerInfo lpInfo) { lpInfo.TimeSinceLastReceivedMessage = TimeSpan.Zero; foreach (CommandHandlerBase cmdHandler in hostCommandHandlers) { if (cmdHandler.Handle(lpInfo.Name, data)) return; } Logger.Log("Unknown LAN command from " + lpInfo.ToString() + " : " + data); } private void CleanUpPlayer(LANPlayerInfo lpInfo) { lpInfo.MessageReceived -= LpInfo_MessageReceived; lpInfo.ConnectionLost -= LpInfo_ConnectionLost; lpInfo.TcpClient.Close(); } #endregion private void HandleServerCommunication() { byte[] message = new byte[1024]; var msg = string.Empty; int bytesRead = 0; int mySessionId = sessionId; if (!client.Connected) return; var stream = client.GetStream(); while (true) { bytesRead = 0; try { bytesRead = stream.Read(message, 0, message.Length); } catch (Exception ex) { // Disconnect from server if (leaving) break; Logger.Log(string.Format( "Reading data from the server failed! Server address: {0}. Exception: {1}", hostEndPoint.Address.ToString(), ex.ToString())); string localizedMessage = string.Format( "Reading data from the server failed! Server address: {0}. Exception: {1}".L10N("Client:Main:LanServerReadError"), hostEndPoint.Address.ToString(), ex.Message); AddCallback(() => { if (sessionId == mySessionId) LeaveGame(localizedMessage); }); break; } if (bytesRead > 0) { msg = encoding.GetString(message, 0, bytesRead); msg = overMessage + msg; List commands = new List(); while (true) { int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); if (index == -1) { overMessage = msg; break; } else { commands.Add(msg.Substring(0, index)); msg = msg.Substring(index + 1); } } foreach (string cmd in commands) { string capturedCmd = cmd; AddCallback(() => { if (sessionId == mySessionId) HandleMessageFromServer(capturedCmd); }); } continue; } // Disconnect from server if (leaving) break; { Logger.Log(string.Format( "Reading data from the server failed (0 bytes received)! Server address: {0}", hostEndPoint.Address.ToString())); string localizedMessage = string.Format( "Reading data from the server failed (0 bytes received)! Server address: {0}".L10N("Client:Main:LanServerReadZero"), hostEndPoint.Address.ToString()); AddCallback(() => { if (sessionId == mySessionId) LeaveGame(localizedMessage); }); } break; } } private void HandleMessageFromServer(string message) { timeSinceLastReceivedCommand = TimeSpan.Zero; foreach (var cmdHandler in playerCommandHandlers) { if (cmdHandler.Handle(message)) return; } Logger.Log("Unknown LAN command from the server: " + message); } protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGame(); protected void LeaveGame(string message = null) { if (leaving) return; Clear(); GameLeft?.Invoke(this, new GameLeftEventArgs() { Message = message }); PlayerExtraOptionsPanel?.Disable(); Disable(); } protected override void UpdateDiscordPresence(bool resetTimer = false) { if (discordHandler == null) return; PlayerInfo player = FindLocalPlayer(); if (player == null || Map == null || GameMode == null) return; string side = ""; if (ddPlayerSides.Length > Players.IndexOf(player)) side = (string)ddPlayerSides[Players.IndexOf(player)].SelectedItem.Tag; string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings discordHandler.UpdatePresence( Map.UntranslatedName, GameMode.UntranslatedUIName, "LAN", currentState, Players.Count, 8, side, "LAN Game", IsHost, false, Locked, resetTimer); } public override void Clear() { if (IsHost) { GameBroadcast?.Invoke(this, new GameBroadcastEventArgs("GAMECLOSED")); BroadcastMessage(PLAYER_QUIT_COMMAND); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); listener.Stop(); } else { SendMessageToHost(PLAYER_QUIT_COMMAND); } base.Clear(); leaving = true; if (this.client.Connected) this.client.Close(); ResetDiscordPresence(); } public void SetChatColorIndex(int colorIndex) { chatColorIndex = colorIndex; tbChatInput.TextColor = chatColors[colorIndex].XNAColor; } public override string GetSwitchName() => "LAN Game Lobby".L10N("Client:Main:LANGameLobby"); protected override void AddNotice(string message, Color color) => lbChatMessages.AddMessage(null, message, color); protected override void BroadcastPlayerOptions() { if (!IsHost) return; var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_BROADCAST_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; foreach (PlayerInfo pInfo in Players.Concat(AIPlayers)) { sb.Append(pInfo.Name); sb.Append(pInfo.SideId); sb.Append(pInfo.ColorId); sb.Append(pInfo.StartingLocation); sb.Append(pInfo.TeamId); if (pInfo.AutoReady && !pInfo.IsInGame && !LastMapChangeWasInvalid) sb.Append(2); else sb.Append(Convert.ToInt32(pInfo.IsAI || pInfo.Ready)); sb.Append(pInfo.IPAddress); if (pInfo.IsAI) sb.Append(pInfo.AILevel); else sb.Append("-1"); } BroadcastMessage(sb.ToString()); } protected override void BroadcastPlayerExtraOptions() { var playerExtraOptions = GetPlayerExtraOptions(); BroadcastMessage(playerExtraOptions.ToLanMessage(), true); } protected override void HostLaunchGame() => BroadcastMessage(LAUNCH_GAME_COMMAND + " " + UniqueGameID); protected override string GetIPAddressForPlayer(PlayerInfo player) { var lpInfo = (LANPlayerInfo)player; return lpInfo.IPAddress; } protected override void RequestPlayerOptions(int side, int color, int start, int team) { var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_REQUEST_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(side); sb.Append(color); sb.Append(start); sb.Append(team); SendMessageToHost(sb.ToString()); } protected override void RequestReadyStatus() => SendMessageToHost(PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked)); protected override void SendChatMessage(string message) { var sb = new ExtendedStringBuilder(CHAT_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(chatColorIndex); sb.Append(message); SendMessageToHost(sb.ToString()); } protected override void OnGameOptionChanged() { base.OnGameOptionChanged(); if (!IsHost) return; var sb = new ExtendedStringBuilder(GAME_OPTIONS_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; foreach (GameLobbyCheckBox chkBox in CheckBoxes) { sb.Append(Convert.ToInt32(chkBox.Checked)); } foreach (GameLobbyDropDown dd in DropDowns) { sb.Append(dd.SelectedIndex); } sb.Append(RandomSeed); sb.Append(Map?.SHA1 ?? string.Empty); sb.Append(GameMode?.Name ?? string.Empty); sb.Append(FrameSendRate); sb.Append(Convert.ToInt32(RemoveStartingLocations)); BroadcastMessage(sb.ToString()); } protected override void GetReadyNotification() { base.GetReadyNotification(); #if WINFORMS WindowManager.FlashWindow(); #endif if (IsHost) BroadcastMessage(GET_READY_COMMAND); } protected override void ClearPingIndicators() { // TODO Implement pings for LAN lobbies } protected override void UpdatePlayerPingIndicator(PlayerInfo pInfo) { // TODO Implement pings for LAN lobbies } /// /// Broadcasts a command to all players in the game as the game host. /// /// The command to send. /// If true, only send this to other players. Otherwise, even the sender will receive their message. private void BroadcastMessage(string message, bool otherPlayersOnly = false) { if (!IsHost) return; foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) { var lpInfo = (LANPlayerInfo)pInfo; lpInfo.SendMessage(message); } } protected override void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e) { base.PlayerExtraOptions_OptionsChanged(sender, e); BroadcastPlayerExtraOptions(); } private void SendMessageToHost(string message) { if (!client.Connected) return; byte[] buffer = encoding.GetBytes(message + ProgramConstants.LAN_MESSAGE_SEPARATOR); NetworkStream ns = client.GetStream(); try { ns.Write(buffer, 0, buffer.Length); ns.Flush(); } catch { Logger.Log("Sending message to game host failed!"); } } protected override void UnlockGame(bool manual) { Locked = false; btnLockGame.Text = "Lock Game".L10N("Client:Main:LockGame"); if (manual) AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnlockedByYou")); } protected override void LockGame() { Locked = true; btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); if (Locked) AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); } protected override void GameProcessExited() { base.GameProcessExited(); SendMessageToHost(RETURN_COMMAND); if (IsHost) { RandomSeed = random.Next(); OnGameOptionChanged(); ClearReadyStatuses(); CopyPlayerDataToUI(); BroadcastPlayerOptions(); BroadcastPlayerExtraOptions(); if (Players.Count < MAX_PLAYER_COUNT) { UnlockGame(true); } } } private void ReturnNotification(string sender) { AddNotice(string.Format("{0} has returned from the game.".L10N("Client:Main:PlayerReturned"), sender)); PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo != null) pInfo.IsInGame = false; sndReturnSound.Play(); CopyPlayerDataToUI(); } public override void Update(GameTime gameTime) { if (IsHost) { for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; if (!lpInfo.Update(gameTime)) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); BroadcastPlayerOptions(); BroadcastPlayerExtraOptions(); UpdateDiscordPresence(); i--; } } timeSinceGameBroadcast += gameTime.ElapsedGameTime; if (timeSinceGameBroadcast > TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL)) { BroadcastGame(); timeSinceGameBroadcast = TimeSpan.Zero; } } else { timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) { string localizedMessage = string.Format( "Connection to the game host timed out. Server address: {0}".L10N("Client:Main:HostConnectTimeOutWithAddress"), hostEndPoint.Address.ToString()); LobbyNotification?.Invoke(this, new LobbyNotificationEventArgs(localizedMessage)); LeaveGame(localizedMessage); } } base.Update(gameTime); } private void BroadcastGame() { var sb = new ExtendedStringBuilder("GAME ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION); sb.Append(ProgramConstants.GAME_VERSION); sb.Append(localGame); sb.Append(Map?.UntranslatedName ?? string.Empty); sb.Append(GameMode?.UntranslatedUIName ?? string.Empty); sb.Append(0); // LoadedGameID var sbPlayers = new StringBuilder(); Players.ForEach(p => sbPlayers.Append(p.Name + ",")); sbPlayers.Remove(sbPlayers.Length - 1, 1); sb.Append(sbPlayers.ToString()); sb.Append(Convert.ToInt32(Locked)); sb.Append(0); // IsLoadedGame sb.Append(Map?.SHA1); GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString())); } #region Command Handlers private void GameHost_HandleChatCommand(string sender, string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length < 2) return; int colorIndex = Conversions.IntFromString(parts[0], -1); if (colorIndex < 0 || colorIndex >= chatColors.Length) return; BroadcastMessage(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); } private void Player_HandleChatCommand(string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length < 3) return; string playerName = parts[0]; int colorIndex = Conversions.IntFromString(parts[1], -1); if (colorIndex < 0 || colorIndex >= chatColors.Length) return; lbChatMessages.AddMessage(new ChatMessage(playerName, chatColors[colorIndex].XNAColor, DateTime.Now, parts[2])); } private void GameHost_HandleReturnCommand(string sender) { BroadcastMessage(RETURN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + sender); } private void Player_HandleReturnCommand(string sender) { ReturnNotification(sender); } private void HandleGetReadyCommand() { if (!IsHost) GetReadyNotification(); } private void HandleHostQuit() { if (!IsHost && !leaving) LeaveGame(); } private void HandlePlayerOptionsRequest(string sender, string data) { if (!IsHost) return; PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length != 4) return; int side = Conversions.IntFromString(parts[0], -1); int color = Conversions.IntFromString(parts[1], -1); int start = Conversions.IntFromString(parts[2], -1); int team = Conversions.IntFromString(parts[3], -1); if (side < 0 || side > SideCount + RandomSelectorCount) return; if (color < 0 || color > MPColors.Count) return; var disallowedSides = GetDisallowedSides(); if (side > 0 && side <= SideCount && disallowedSides[side - 1]) return; if (GameModeMap.CoopInfo != null) { if (GameModeMap.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; if (GameModeMap.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; } if (!(start == 0 || (GameModeMap?.AllowedStartingLocations?.Contains(start) ?? true))) return; if (team < 0 || team > 4) return; if (side != pInfo.SideId || start != pInfo.StartingLocation || team != pInfo.TeamId) { ClearReadyStatuses(); } pInfo.SideId = side; pInfo.ColorId = color; pInfo.StartingLocation = start; pInfo.TeamId = team; CopyPlayerDataToUI(); BroadcastPlayerOptions(); } private void HandlePlayerExtraOptionsBroadcast(string data) => ApplyPlayerExtraOptions(null, data); private void HandlePlayerOptionsBroadcast(string data) { if (IsHost) return; string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); int playerCount = parts.Length / 8; if (parts.Length != playerCount * 8) return; PlayerInfo localPlayer = FindLocalPlayer(); int oldSideId = localPlayer == null ? -1 : localPlayer.SideId; Players.Clear(); AIPlayers.Clear(); for (int i = 0; i < playerCount; i++) { int baseIndex = i * 8; string name = parts[baseIndex]; int side = Conversions.IntFromString(parts[baseIndex + 1], -1); int color = Conversions.IntFromString(parts[baseIndex + 2], -1); int start = Conversions.IntFromString(parts[baseIndex + 3], -1); int team = Conversions.IntFromString(parts[baseIndex + 4], -1); int readyStatus = Conversions.IntFromString(parts[baseIndex + 5], -1); string ipAddress = parts[baseIndex + 6]; int aiLevel = Conversions.IntFromString(parts[baseIndex + 7], -1); if (side < 0 || side > SideCount + RandomSelectorCount) return; if (color < 0 || color > MPColors.Count) return; if (start < 0 || start > MAX_PLAYER_COUNT) return; if (team < 0 || team > 4) return; if (ipAddress == "127.0.0.1") ipAddress = hostEndPoint.Address.ToString(); bool isAi = aiLevel > -1; if (aiLevel > 2) return; PlayerInfo pInfo; if (!isAi) { pInfo = new LANPlayerInfo(encoding); pInfo.Name = name; Players.Add(pInfo); } else { pInfo = new PlayerInfo(); pInfo.Name = AILevelToName(aiLevel); pInfo.IsAI = true; pInfo.AILevel = aiLevel; AIPlayers.Add(pInfo); } pInfo.SideId = side; pInfo.ColorId = color; pInfo.StartingLocation = start; pInfo.TeamId = team; pInfo.Ready = readyStatus > 0; pInfo.AutoReady = readyStatus > 1; pInfo.IPAddress = ipAddress; } CopyPlayerDataToUI(); localPlayer = FindLocalPlayer(); if (localPlayer != null && oldSideId != localPlayer.SideId) UpdateDiscordPresence(); } private void HandlePlayerQuit(string sender) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), pInfo.Name)); Players.Remove(pInfo); ClearReadyStatuses(); CopyPlayerDataToUI(); BroadcastPlayerOptions(); UpdateDiscordPresence(); } private void HandleGameOptionsMessage(string data) { if (IsHost) return; string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length != CheckBoxes.Count + DropDowns.Count + GAME_OPTION_SPECIAL_FLAG_COUNT) { AddNotice(("The game host has sent an invalid game options message! " + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid")); Logger.Log("Invalid game options message from host: " + data); return; } int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1); if (randomSeed == -1) return; RandomSeed = randomSeed; string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; GameModeMap gameModeMap = GameModeMaps.FirstOrDefault(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); if (gameModeMap == null) { ChangeMap(null); if (!string.IsNullOrEmpty(mapSHA1)) AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + "The host needs to change the map or you won't be able to play.".L10N("Client:Main:HostNeedChangeMapForYou")); return; } if (GameModeMap != gameModeMap) ChangeMap(gameModeMap); int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); if (frameSendRate != FrameSendRate) { FrameSendRate = frameSendRate; AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); } bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString( parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); SetRandomStartingLocations(removeStartingLocations); for (int i = 0; i < CheckBoxes.Count; i++) { GameLobbyCheckBox chkBox = CheckBoxes[i]; bool oldValue = chkBox.Checked; chkBox.Checked = Conversions.IntFromString(parts[i], -1) > 0; if (chkBox.Checked != oldValue) { if (chkBox.Checked) AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), chkBox.Text)); else AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), chkBox.Text)); } } for (int i = 0; i < DropDowns.Count; i++) { int index = Conversions.IntFromString(parts[CheckBoxes.Count + i], -1); GameLobbyDropDown dd = DropDowns[i]; if (index < 0 || index >= dd.Items.Count) return; int oldValue = dd.SelectedIndex; dd.SelectedIndex = index; if (index != oldValue) { string ddName = dd.OptionName; if (dd.OptionName == null) ddName = dd.Name; AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); } } } private void GameHost_HandleReadyRequest(string sender, string autoReady) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); if (pInfo == null) return; pInfo.Ready = true; pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0)); CopyPlayerDataToUI(); BroadcastPlayerOptions(); } private void HandleGameLaunchCommand(string gameId) { Players.ForEach(pInfo => pInfo.IsInGame = true); UniqueGameID = Conversions.IntFromString(gameId, -1); if (UniqueGameID < 0) return; CopyPlayerDataToUI(); StartGame(); } private void HandlePing() { SendMessageToHost(PING); } protected override void BroadcastDiceRoll(int dieSides, int[] results) { string resultString = string.Join(",", results); SendMessageToHost($"DR {dieSides},{resultString}"); } private void Host_HandleDiceRoll(string sender, string result) { BroadcastMessage($"{DICE_ROLL_COMMAND} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}"); } private void Client_HandleDiceRoll(string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length != 2) return; HandleDiceRollResult(parts[0], parts[1]); } #endregion protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); iniFile.SetIntValue("Settings", "Port", ProgramConstants.LAN_INGAME_PORT); iniFile.SetIntValue("Settings", "GameID", UniqueGameID); iniFile.SetBooleanValue("Settings", "Host", IsHost); } } public class LobbyNotificationEventArgs : EventArgs { public LobbyNotificationEventArgs(string notification) { Notification = notification; } public string Notification { get; private set; } } public class GameBroadcastEventArgs : EventArgs { public GameBroadcastEventArgs(string message) { Message = message; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/MapCodeHelper.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using ClientCore; using ClientCore.I18N; using ClientCore.Extensions; using DTAClient.Domain.Multiplayer; using Rampastring.Tools; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public static class MapCodeHelper { public static Encoding GetMapEncoding(string filepath) => Translation.Instance.MapEncoding ?? FileExtensions.GetDetectedEncoding(filepath); /// /// Applies code from a component custom INI file to a map INI file. /// /// The map INI file. /// The custom INI file path. /// Currently selected gamemode, if set. public static void ApplyMapCode(IniFile mapIni, string customIniPath, GameMode gameMode) { string associatedIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, customIniPath); Encoding associatedIniEncoding = GetMapEncoding(associatedIniPath); IniFile associatedIni = new IniFile(associatedIniPath, associatedIniEncoding); string extraIniName = null; if (gameMode != null) extraIniName = associatedIni.GetStringValue("GameModeIncludes", gameMode.Name, null); associatedIni.EraseSectionKeys("GameModeIncludes"); ApplyMapCode(mapIni, associatedIni); if (!String.IsNullOrEmpty(extraIniName)) { string extraIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, extraIniName); Encoding extraIniEncoding = GetMapEncoding(extraIniPath); ApplyMapCode(mapIni, new IniFile(extraIniPath, extraIniEncoding)); } } /// /// Apply map code from an arbitrary INI file to a map INI file. /// /// The map INI file. /// The INI file to apply to map INI file. public static void ApplyMapCode(IniFile mapIni, IniFile mapCodeIni) { ReplaceMapObjects(mapIni, mapCodeIni, "Aircraft"); ReplaceMapObjects(mapIni, mapCodeIni, "Infantry"); ReplaceMapObjects(mapIni, mapCodeIni, "Units"); ReplaceMapObjects(mapIni, mapCodeIni, "Structures"); ReplaceMapObjects(mapIni, mapCodeIni, "Terrain"); IniFile.ConsolidateIniFiles(mapIni, mapCodeIni); } /// /// Replace all instances of objects defined in specific map section that match ID's with new object ID's. /// /// The map INI file. /// The INI file to apply to map INI file. /// The object section ID. private static void ReplaceMapObjects(IniFile mapIni, IniFile mapCodeIni, string sectionName) { string replaceSectionName = "ReplaceMap" + sectionName; List> objectRemapPairs = GetKeyValuePairs(mapCodeIni, replaceSectionName); if (objectRemapPairs.Count < 1) return; List> sectionKeyValuePairs = GetKeyValuePairs(mapIni, sectionName); foreach (KeyValuePair objectRemapPair in objectRemapPairs) { List> matchingSectionKVPs = sectionKeyValuePairs.Where(x => GetObjectID(x.Value, sectionName) == objectRemapPair.Key).ToList(); foreach (KeyValuePair matchingSectionKVP in matchingSectionKVPs) { string id = GetObjectID(matchingSectionKVP.Value, sectionName); if (!String.IsNullOrEmpty(objectRemapPair.Value)) { mapIni.SetStringValue(sectionName, matchingSectionKVP.Key, matchingSectionKVP.Value.Replace(id, objectRemapPair.Value)); Logger.Log("MapCodeHelper: Changed an instance of '" + sectionName + "' object '" + id + "' into '" + objectRemapPair.Value + "'."); } else { mapIni.SetStringValue(sectionName, matchingSectionKVP.Key, ""); Logger.Log("MapCodeHelper: Removed an instance of '" + sectionName + "' object '" + id + "'."); } } } mapCodeIni.EraseSectionKeys(replaceSectionName); } /// /// Get object ID from an object section value. /// /// Object section value. /// Section ID. /// private static string GetObjectID(string value, string sectionName) { if (sectionName != "Terrain") { string[] splitValue = value.Split(','); if (splitValue.Length < 2) return "N/A"; else return splitValue[1]; } else return value; } /// /// Get key/value pairs from ini file section. /// /// Ini file. /// Ini file section. /// List of key/value pairs from the chosen ini file section. If ini file section has no keys, an empty list is returned. private static List> GetKeyValuePairs(IniFile iniFile, string sectionName) { IniSection section = iniFile.GetSection(sectionName); if (section == null) return new List>(); return section.Keys; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs ================================================ using ClientCore; using DTAClient.Domain.Multiplayer; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.IO; using System.Linq; using ClientGUI; using ClientCore.Extensions; using System.Diagnostics; using Color = Microsoft.Xna.Framework.Color; using Image = SixLabors.ImageSharp.Image; namespace DTAClient.DXGUI.Multiplayer.GameLobby { struct MapPreviewBoxExtraMapPreviewTexture { public Texture2D Texture; public Point ControlAreaPoint; public bool Toggleable; public MapPreviewBoxExtraMapPreviewTexture(Texture2D texture, Point point, bool toggleable) { Texture = texture; ControlAreaPoint = point; Toggleable = toggleable; } } /// /// The picture box for displaying the map preview. /// public class MapPreviewBox : XNAPanel, ICompositeControl { public IReadOnlyList SubControls => [CoopBriefingBox]; private const int MAX_STARTING_LOCATIONS = 8; public delegate void LocalStartingLocationSelectedEventHandler(object sender, LocalStartingLocationEventArgs e); public event EventHandler LocalStartingLocationSelected; public event EventHandler StartingLocationApplied; private readonly MapLoader mapLoader; public MapPreviewBox(WindowManager windowManager, MapLoader mapLoader) : base(windowManager) { PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; FontIndex = 1; CoopBriefingBox = new CoopBriefingBox(windowManager); CoopBriefingBox.DrawOrder = 100; // not a magic number, just high enough so don't need to change it later CoopBriefingBox.UpdateOrder = 100; CoopBriefingBox.Disable(); NameChanged += MapPreviewBox_NameChanged; this.mapLoader = mapLoader; } private void MapPreviewBox_NameChanged(object sender, EventArgs e) { CoopBriefingBox.Name = $"{Name}_CoopBriefingBox"; } public void SetFields(List players, List aiPlayers, List mpColors, string[] sides, IniFile gameOptionsIni) { this.players = players; this.aiPlayers = aiPlayers; this.mpColors = mpColors; this.sides = sides; this.gameOptionsIni = gameOptionsIni; Color nameBackgroundColor = AssetLoader.GetRGBAColorFromString( ClientConfiguration.Instance.MapPreviewNameBackgroundColor); Color nameBorderColor = AssetLoader.GetRGBAColorFromString( ClientConfiguration.Instance.MapPreviewNameBorderColor); double angularVelocity = gameOptionsIni.GetDoubleValue("General", "StartingLocationAngularVelocity", 0.015); double reservedAngularVelocity = gameOptionsIni.GetDoubleValue("General", "ReservedStartingLocationAngularVelocity", -0.0075); Color hoverRemapColor = AssetLoader.GetRGBAColorFromString(ClientConfiguration.Instance.MapPreviewStartingLocationHoverRemapColor); startingLocationIndicators = new PlayerLocationIndicator[MAX_STARTING_LOCATIONS]; // Init starting location indicators for (int i = 0; i < MAX_STARTING_LOCATIONS; i++) { PlayerLocationIndicator indicator = new PlayerLocationIndicator(WindowManager, mpColors, nameBackgroundColor, nameBorderColor, contextMenu); indicator.FontIndex = FontIndex; indicator.Visible = false; indicator.Enabled = false; indicator.AngularVelocity = angularVelocity; indicator.HoverRemapColor = hoverRemapColor; indicator.ReversedAngularVelocity = reservedAngularVelocity; indicator.WaypointTexture = AssetLoader.LoadTexture(string.Format("slocindicator{0}.png", i + 1)); indicator.Tag = i; indicator.LeftClick += Indicator_LeftClick; indicator.RightClick += Indicator_RightClick; startingLocationIndicators[i] = indicator; AddChild(indicator); } ClientRectangleUpdated += (s, e) => UpdateMap(); } private GameModeMap _gameModeMap; public GameModeMap GameModeMap { get => _gameModeMap; set { _gameModeMap = value; UpdateMap(); } } public int FontIndex { get; set; } /// /// Controls whether the context menu is enabled for this map preview box. /// Skirmish games and online games where the local player is the host should /// set have this set to true. /// public bool EnableContextMenu { get; set; } public bool EnableStartLocationSelection { get; set; } private readonly string[] teamIds = new[] { string.Empty } .Concat(ProgramConstants.TEAMS.Select(team => $"[{team}]")).ToArray(); private string[] sides; public int RandomSelectorCount { get; set; } private PlayerLocationIndicator[] startingLocationIndicators; private List mpColors; private List players; private List aiPlayers; private XNAContextMenu mainContextMenu; private XNAContextMenu contextMenu; private Point lastContextMenuPoint; private XNAContextMenu mapContextMenu; private XNAContextMenuItem toggleFavoriteMapItem; private XNAContextMenuItem toggleExtraTexturesItem; private XNAContextMenuItem showInFolderItem; private XNAClientButton btnToggleFavoriteMap; private XNAClientButton btnToggleExtraTextures; private CoopBriefingBox CoopBriefingBox; private Rectangle textureRectangle; /// /// Indicates whether `mapPreviewTexture` needs to be disposed before loading the next texture. /// private bool mapPreviewTextureNeedsDispose = false; private Texture2D mapPreviewTexture = null; private bool useNearestNeighbour = false; private IniFile gameOptionsIni; private EnhancedSoundEffect sndClickSound; private EnhancedSoundEffect sndDropdownSound; private List extraTextures = new List(0); public EventHandler ToggleFavorite; public override void Initialize() { EnableStartLocationSelection = true; BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); mainContextMenu = new XNAContextMenu(WindowManager); mainContextMenu.Name = nameof(mainContextMenu); mainContextMenu.ClientRectangle = new Rectangle(0, 0, 150, 2); mainContextMenu.Disable(); AddChild(mainContextMenu); contextMenu = new XNAContextMenu(WindowManager); contextMenu.Tag = -1; contextMenu.ClientRectangle = new Rectangle(0, 0, 150, 2); AddChild(contextMenu); contextMenu.Disable(); toggleFavoriteMapItem = new XNAContextMenuItem() { Text = "Add Favorite".L10N("Client:Main:AddFavorite"), SelectAction = ToggleFavoriteMap, SelectableChecker = () => GameModeMap != null }; toggleExtraTexturesItem = new XNAContextMenuItem() { Text = "Hide Extra Icons".L10N("Client:Main:HideExtraIcons"), SelectAction = ToggleExtraTextures, SelectableChecker = () => GameModeMap != null, VisibilityChecker = () => extraTextures.Any(x => x.Toggleable) }; showInFolderItem = new XNAContextMenuItem() { Text = "Show in folder".L10N("Client:Main:ShowInFolder"), SelectAction = ShowInFolder, SelectableChecker = () => GameModeMap != null }; mapContextMenu = new XNAContextMenu(WindowManager); mapContextMenu.ClientRectangle = new Rectangle(0, 0, 120, 2); mapContextMenu.AddItem(toggleFavoriteMapItem); mapContextMenu.AddItem(toggleExtraTexturesItem); mapContextMenu.AddItem(showInFolderItem); btnToggleFavoriteMap = new XNAClientButton(WindowManager); btnToggleFavoriteMap.IdleTexture = AssetLoader.LoadTexture("favInactive.png"); btnToggleFavoriteMap.LeftClick += (sender, args) => ToggleFavorite?.Invoke(sender, args); btnToggleFavoriteMap.ToolTipText = "Toggle Favorite Map".L10N("Client:Main:ToggleFavoriteMap"); btnToggleExtraTextures = new XNAClientButton(WindowManager); btnToggleExtraTextures.IdleTexture = AssetLoader.LoadTexture("pvTexturesActive.png"); btnToggleExtraTextures.LeftClick += (sender, args) => ToggleExtraTextures(); btnToggleExtraTextures.ToolTipText = "Toggle Extra Icons".L10N("Client:Main:ToggleExtraIcons"); btnToggleExtraTextures.Disable(); AddChild(mapContextMenu); mapContextMenu.Disable(); // this is needed for the control composition to work properly, as otherwise // the controls will be initialized twice via INItializableWindow system AddChildWithoutInitialize(CoopBriefingBox); sndClickSound = new EnhancedSoundEffect("button.wav"); sndDropdownSound = new EnhancedSoundEffect("dropdown.wav"); base.Initialize(); ClientRectangleUpdated += (s, e) => UpdateMap(); RightClick += MapPreviewBox_RightClick; AddChild(btnToggleFavoriteMap); AddChild(btnToggleExtraTextures); } private void MapPreviewBox_RightClick(object sender, EventArgs e) { if (GameModeMap == null) return; toggleFavoriteMapItem.Text = GameModeMap.IsFavorite ? "Remove Favorite".L10N("Client:Main:RemoveFavorite") : "Add Favorite".L10N("Client:Main:AddFavorite"); toggleExtraTexturesItem.Text = UserINISettings.Instance.DisplayToggleableExtraTextures ? "Hide Extra Icons".L10N("Client:Main:HideExtraIcons") : "Show Extra Icons".L10N("Client:Main:ShowExtraIcons"); mapContextMenu.Open(GetCursorPoint()); } private void ToggleFavoriteMap() { ToggleFavorite?.Invoke(null, null); } private void ToggleExtraTextures() { UserINISettings.Instance.DisplayToggleableExtraTextures.Value = !UserINISettings.Instance.DisplayToggleableExtraTextures; RefreshExtraTexturesBtn(); } private void ShowInFolder() => GameModeMap?.Map.OpenContainingFolder(); private void ContextMenu_OptionSelected(int index) { SoundPlayer.Play(sndDropdownSound); if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in players.Concat(aiPlayers)) { if (pInfo.StartingLocation == (int)contextMenu.Tag + 1) pInfo.StartingLocation = 0; } } PlayerInfo player; if (index >= players.Count) { int aiIndex = index - players.Count; if (aiIndex >= aiPlayers.Count) return; player = aiPlayers[aiIndex]; } else player = players[index]; player.StartingLocation = (int)contextMenu.Tag + 1; StartingLocationApplied?.Invoke(this, EventArgs.Empty); } /// /// Allows the user to select their starting location by clicking on one of them /// in the map preview. /// private void Indicator_LeftClick(object sender, EventArgs e) { if (!EnableStartLocationSelection) return; var indicator = (PlayerLocationIndicator)sender; SoundPlayer.Play(sndClickSound); if (!EnableContextMenu) { if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in players.Concat(aiPlayers)) { if (pInfo.StartingLocation == (int)indicator.Tag + 1) return; } } LocalStartingLocationSelected?.Invoke(this, new LocalStartingLocationEventArgs((int)indicator.Tag + 1)); return; } //if (contextMenu.Visible) //{ // contextMenu.Visible = false; // contextMenu.Enabled = false; // return; //} //if (Map.EnforceMaxPlayers) //{ // foreach (PlayerInfo pInfo in players.Concat(aiPlayers)) // { // if (pInfo.StartingLocation == (int)indicator.Tag + 1) // return; // } //} int x = indicator.Right; int y = indicator.Y; if (x + contextMenu.Width > Width) x = indicator.X - contextMenu.Width; if (y + contextMenu.Height > Height) y = Height - contextMenu.Height; contextMenu.Tag = indicator.Tag; int index = 0; foreach (PlayerInfo pInfo in players.Concat(aiPlayers)) { contextMenu.Items[index].Selectable = pInfo.StartingLocation != (int)indicator.Tag + 1 && pInfo.SideId < sides.Length + RandomSelectorCount; index++; } lastContextMenuPoint = new Point(x, y); contextMenu.Open(lastContextMenuPoint); } private void Indicator_RightClick(object sender, EventArgs e) { var indicator = (PlayerLocationIndicator)sender; if (!EnableContextMenu) { PlayerInfo pInfo = players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (pInfo.StartingLocation == (int)indicator.Tag + 1) { LocalStartingLocationSelected?.Invoke(this, new LocalStartingLocationEventArgs(0)); } return; } foreach (PlayerInfo pInfo in players.Union(aiPlayers)) { if (pInfo.StartingLocation == (int)indicator.Tag + 1) pInfo.StartingLocation = 0; } StartingLocationApplied?.Invoke(this, EventArgs.Empty); } /// /// Updates the map preview texture's position inside /// this control's display rectangle and the /// starting location indicators' positions. /// private void UpdateMap() { if (mapPreviewTextureNeedsDispose && mapPreviewTexture != null && !mapPreviewTexture.IsDisposed) { mapPreviewTexture.Dispose(); mapPreviewTextureNeedsDispose = false; } extraTextures.Clear(); if (GameModeMap == null) { mapPreviewTexture = null; CoopBriefingBox.Disable(); contextMenu.Disable(); foreach (var indicator in startingLocationIndicators) indicator.Disable(); return; } Debug.Assert(!mapPreviewTextureNeedsDispose, "previous texture must be disposed before loading a new texture"); Image previewTextureImage = mapLoader.GetCachedPreviewImageFromMap(GameModeMap.Map, syncLoadOnCacheMiss: true); mapPreviewTexture = previewTextureImage != null ? AssetLoader.TextureFromImage(previewTextureImage) // This null case indicates a "hidden preview", where the map itself intends not to show a preview, so we just show a black box instead of no texture at all. // Use the same `- 2` to let xRatio and yRatio get calculated as 1. : AssetLoader.CreateTexture(Color.Black, Width - 2, Height - 2); mapPreviewTextureNeedsDispose = true; if (!string.IsNullOrEmpty(GameModeMap.Map.Briefing)) { CoopBriefingBox.SetText(GameModeMap.Map.Briefing); CoopBriefingBox.Enable(); if (IsActive) CoopBriefingBox.SetAlpha(0f); } else CoopBriefingBox.Disable(); double xRatio = (Width - 2) / (double)mapPreviewTexture.Width; double yRatio = (Height - 2) / (double)mapPreviewTexture.Height; double ratio; int texturePositionX = 1; int texturePositionY = 1; int textureHeight = 0; int textureWidth = 0; if (xRatio > yRatio) { ratio = yRatio; textureHeight = Height - 2; textureWidth = (int)(mapPreviewTexture.Width * ratio); texturePositionX = (int)(Width - 2 - textureWidth) / 2; } else { ratio = xRatio; textureWidth = Width - 2; textureHeight = (int)(mapPreviewTexture.Height * ratio); texturePositionY = (Height - 2 - textureHeight) / 2 + 1; } useNearestNeighbour = ratio < 1.0; textureRectangle = new Rectangle(texturePositionX, texturePositionY, textureWidth, textureHeight); List startingLocations = GameModeMap.Map.GetStartingLocationPreviewCoords(new Point(mapPreviewTexture.Width, mapPreviewTexture.Height)); // Disable all indicators to be able updated after changing // locations when 2 or more of them have same location (RA1 specifics) foreach (var indicator in startingLocationIndicators) { indicator.Disable(); } for (int i = 0; i < MAX_STARTING_LOCATIONS; i++) { bool showLocation = i < startingLocations.Count && GameModeMap.AllowedStartingLocations.Contains(i + 1); if (showLocation) { PlayerLocationIndicator indicator = startingLocationIndicators[i]; Point location = new Point( texturePositionX + (int)(startingLocations[i].X * ratio), texturePositionY + (int)(startingLocations[i].Y * ratio)); indicator.SetPosition(location); indicator.Enabled = true; indicator.Visible = true; } else { startingLocationIndicators[i].Disable(); } } foreach (ExtraMapPreviewTexture mapExtraTexture in GameModeMap.Map.GetExtraMapPreviewTextures()) { // LoadTexture makes use of a texture cache // so we don't need to cache the textures manually Texture2D extraTexture = AssetLoader.LoadTexture(mapExtraTexture.TextureName); Point location = PreviewTexturePointToControlAreaPoint( GameModeMap.Map.MapPointToMapPreviewPoint(mapExtraTexture.Point, new Point(mapPreviewTexture.Width - (extraTexture.Width / 2), mapPreviewTexture.Height - (extraTexture.Height / 2)), mapExtraTexture.Level), ratio); extraTextures.Add(new MapPreviewBoxExtraMapPreviewTexture(extraTexture, location, mapExtraTexture.Toggleable)); } int buttonX = Width; if (extraTextures.Any(x => x.Toggleable)) { btnToggleExtraTextures.ClientRectangle = new Rectangle(buttonX - 22, 4, 18, 18); btnToggleExtraTextures.Enable(); buttonX = btnToggleExtraTextures.X; } else { btnToggleExtraTextures.Disable(); } btnToggleFavoriteMap.ClientRectangle = new Rectangle(buttonX - 22, 4, 18, 18); RefreshExtraTexturesBtn(); RefreshFavoriteBtn(); } public void RefreshFavoriteBtn() { bool isFav = UserINISettings.Instance.IsFavoriteMap(GameModeMap?.Map.SHA1, GameModeMap?.Map.UntranslatedName, GameModeMap?.GameMode.Name); var textureName = isFav ? "favActive.png" : "favInactive.png"; var hoverTextureName = isFav ? "favActive_c.png" : "favInactive_c.png"; var hoverTexture = AssetLoader.AssetExists(hoverTextureName) ? AssetLoader.LoadTexture(hoverTextureName) : null; btnToggleFavoriteMap.IdleTexture = AssetLoader.LoadTexture(textureName); btnToggleFavoriteMap.HoverTexture = hoverTexture; } public void RefreshExtraTexturesBtn() { var textureName = UserINISettings.Instance.DisplayToggleableExtraTextures ? "pvTexturesActive.png" : "pvTexturesInactive.png"; var hoverTextureName = UserINISettings.Instance.DisplayToggleableExtraTextures ? "pvTexturesActive_c.png" : "pvTexturesInactive_c.png"; var hoverTexture = AssetLoader.AssetExists(hoverTextureName) ? AssetLoader.LoadTexture(hoverTextureName) : null; btnToggleExtraTextures.IdleTexture = AssetLoader.LoadTexture(textureName); btnToggleExtraTextures.HoverTexture = hoverTexture; } private Point PreviewTexturePointToControlAreaPoint(Point previewTexturePoint, double scaleRatio) { return new Point(textureRectangle.X + (int)(previewTexturePoint.X * scaleRatio), textureRectangle.Y + (int)(previewTexturePoint.Y * scaleRatio)); } public void UpdateStartingLocationTexts() { foreach (PlayerLocationIndicator indicator in startingLocationIndicators) indicator.Players.Clear(); foreach (PlayerInfo pInfo in players) { if (pInfo.StartingLocation > 0) startingLocationIndicators[pInfo.StartingLocation - 1].Players.Add(pInfo); } foreach (PlayerInfo aiInfo in aiPlayers) { if (aiInfo.StartingLocation > 0) startingLocationIndicators[aiInfo.StartingLocation - 1].Players.Add(aiInfo); } foreach (PlayerLocationIndicator indicator in startingLocationIndicators) indicator.Refresh(); contextMenu.ClearItems(); int id = 1; var playerList = players.Concat(aiPlayers).ToList(); for (int i = 0; i < playerList.Count; i++) { PlayerInfo pInfo = playerList[i]; string text = pInfo.Name; if (pInfo.TeamId > 0) { text = teamIds[pInfo.TeamId] + " " + text; } int index = i; XNAContextMenuItem item = new XNAContextMenuItem() { Text = id + ". " + text, TextColor = pInfo.ColorId > 0 ? mpColors[pInfo.ColorId - 1].XnaColor : Color.White, SelectAction = () => ContextMenu_OptionSelected(index), }; contextMenu.AddItem(item); id++; } if (EnableContextMenu && contextMenu.Enabled && contextMenu.Visible) { contextMenu.Disable(); contextMenu.Open(lastContextMenuPoint); } } public override void OnMouseEnter() { foreach (PlayerLocationIndicator indicator in startingLocationIndicators) indicator.BackgroundShown = true; if (GameModeMap != null && !string.IsNullOrEmpty(GameModeMap.Map.Briefing)) { CoopBriefingBox.SetFadeVisibility(false); } else CoopBriefingBox.Disable(); base.OnMouseEnter(); } public override void OnMouseLeave() { foreach (PlayerLocationIndicator indicator in startingLocationIndicators) indicator.BackgroundShown = false; if (GameModeMap != null && !string.IsNullOrEmpty(GameModeMap.Map.Briefing)) { CoopBriefingBox.SetText(GameModeMap.Map.Briefing); CoopBriefingBox.SetFadeVisibility(true); } base.OnMouseLeave(); } public override void OnLeftClick(InputEventArgs inputEventArgs) { inputEventArgs.Handled = true; if (Keyboard.IsKeyHeldDown(Keys.LeftControl)) { FileInfo previewFileInfo = SafePath.GetFile(ProgramConstants.GamePath, GameModeMap.Map.PreviewPath); if (previewFileInfo.Exists) { try { ProcessLauncher.StartShellProcess(previewFileInfo.FullName); } catch { } } } base.OnLeftClick(inputEventArgs); } public override void Draw(GameTime gameTime) { DrawPanel(); if (mapPreviewTexture != null) { Point renderPoint = GetRenderPoint(); if (useNearestNeighbour) { Renderer.PushSettings(new SpriteBatchSettings(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null)); DrawPreviewTexture(renderPoint); Renderer.PopSettings(); } else { DrawPreviewTexture(renderPoint); } if (DrawBorders) DrawPanelBorders(); foreach (var extraTexture in extraTextures) { if (!extraTexture.Toggleable || UserINISettings.Instance.DisplayToggleableExtraTextures) { Renderer.DrawTexture(extraTexture.Texture, new Rectangle(renderPoint.X + extraTexture.ControlAreaPoint.X, renderPoint.Y + extraTexture.ControlAreaPoint.Y, extraTexture.Texture.Width, extraTexture.Texture.Height), Color.White); } } } else if (DrawBorders) { DrawPanelBorders(); } DrawChildren(gameTime); } private void DrawPreviewTexture(Point renderPoint) { Renderer.DrawTexture(mapPreviewTexture, new Rectangle(renderPoint.X + textureRectangle.X, renderPoint.Y + textureRectangle.Y, textureRectangle.Width, textureRectangle.Height), Color.White); } } public class LocalStartingLocationEventArgs : EventArgs { public LocalStartingLocationEventArgs(int startingLocationIndex) { StartingLocationIndex = startingLocationIndex; } public int StartingLocationIndex { get; set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using Microsoft.Xna.Framework; using ClientCore; using System.IO; using Rampastring.Tools; using ClientCore.Statistics; using DTAClient.DXGUI.Generic; using DTAClient.Domain.Multiplayer; using ClientGUI; using System.Text; using DTAClient.Domain; using Microsoft.Xna.Framework.Graphics; using ClientCore.Extensions; using DTAClient.DXGUI.Multiplayer.CnCNet; using System.Diagnostics; namespace DTAClient.DXGUI.Multiplayer.GameLobby { /// /// A generic base class for multiplayer game lobbies (CnCNet and LAN). /// public abstract class MultiplayerGameLobby : GameLobbyBase, ISwitchable { private const int MAX_DICE = 10; private const int MAX_DIE_SIDES = 100; public MultiplayerGameLobby(WindowManager windowManager, string iniName, TopBar topBar, MapLoader mapLoader, DiscordHandler discordHandler, PrivateMessagingWindow pmWindow, Random random) : base(windowManager, iniName, mapLoader, true, discordHandler, random) { TopBar = topBar; this.random = random; chatBoxCommands = new List { new ChatBoxCommand("HIDEMAPS", "Hide map list (game host only)".L10N("Client:Main:ChatboxCommandHideMapsHelp"), true, s => HideMapList()), new ChatBoxCommand("SHOWMAPS", "Show map list (game host only)".L10N("Client:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), new ChatBoxCommand("FRAMESENDRATE", string.Format("Change order lag / FrameSendRate (default {0}) (game host only)".L10N("Client:Main:ChatboxCommandFrameSendRateHelpV2"), ClientConfiguration.Instance.DefaultFrameSendRate), true, s => SetFrameSendRate(s)), new ChatBoxCommand("MAXAHEAD", string.Format("Change MaxAhead (default {0}) (game host only)".L10N("Client:Main:ChatboxCommandMaxAheadHelpV2"), ClientConfiguration.Instance.DefaultMaxAhead), true, s => SetMaxAhead(s)), new ChatBoxCommand("PROTOCOLVERSION", string.Format("Change ProtocolVersion (default {0}) (game host only)".L10N("Client:Main:ChatboxCommandProtocolVersionHelpV2"), ClientConfiguration.Instance.DefaultProtocolVersion), true, s => SetProtocolVersion(s)), new ChatBoxCommand("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("Client:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), new ChatBoxCommand("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("Client:Main:ChatboxCommandRandomStartsHelp"), true, s => SetStartingLocationClearance(s)), new ChatBoxCommand("ROLL", "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, RollDiceCommand), new ChatBoxCommand("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("Client:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), new ChatBoxCommand("LOADOPTIONS", "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, HandleGameOptionPresetLoadCommand) }; } protected XNAPlayerSlotIndicator[] StatusIndicators; protected ChatListBox lbChatMessages; protected XNAChatTextBox tbChatInput; protected XNAClientButton btnLockGame; protected XNAClientCheckBox chkAutoReady; private Random random; protected bool IsHost = false; private bool locked = false; protected bool Locked { get => locked; set { bool oldLocked = locked; locked = value; if (oldLocked != value) { CopyPlayerDataToUI(); UpdateDiscordPresence(); } } } // protected bool DisableSpectatorReadyChecking = false; protected EnhancedSoundEffect sndJoinSound; protected EnhancedSoundEffect sndLeaveSound; protected EnhancedSoundEffect sndMessageSound; protected EnhancedSoundEffect sndGetReadySound; protected EnhancedSoundEffect sndReturnSound; protected Texture2D[] PingTextures; protected TopBar TopBar; protected int FrameSendRate { get; set; } /// /// Controls the MaxAhead parameter. The default value of 0 means that /// the value is not written to spawn.ini, which allows the spawner the /// calculate and assign the MaxAhead value. /// protected int MaxAhead { get; set; } protected int ProtocolVersion { get; set; } protected List chatBoxCommands; private FileSystemWatcher fsw; private bool gameSaved = false; protected bool LastMapChangeWasInvalid { get; set; } = false; /// /// Allows derived classes to add their own chat box commands. /// /// The command to add. protected void AddChatBoxCommand(ChatBoxCommand command) => chatBoxCommands.Add(command); public override void Initialize() { Name = nameof(MultiplayerGameLobby); base.Initialize(); // Init default game network settings FrameSendRate = ClientConfiguration.Instance.DefaultFrameSendRate; ProtocolVersion = ClientConfiguration.Instance.DefaultProtocolVersion; MaxAhead = ClientConfiguration.Instance.DefaultMaxAhead; // DisableSpectatorReadyChecking = GameOptionsIni.GetBooleanValue("General", "DisableSpectatorReadyChecking", false); PingTextures = new Texture2D[5] { AssetLoader.LoadTexture("ping0.png"), AssetLoader.LoadTexture("ping1.png"), AssetLoader.LoadTexture("ping2.png"), AssetLoader.LoadTexture("ping3.png"), AssetLoader.LoadTexture("ping4.png") }; InitPlayerOptionDropdowns(); StatusIndicators = new XNAPlayerSlotIndicator[MAX_PLAYER_COUNT]; int statusIndicatorX = ConfigIni.GetIntValue(Name, "PlayerStatusIndicatorX", 0); int statusIndicatorY = ConfigIni.GetIntValue(Name, "PlayerStatusIndicatorY", 0); for (int i = 0; i < MAX_PLAYER_COUNT; i++) { var indicatorPlayerReady = new XNAPlayerSlotIndicator(WindowManager); indicatorPlayerReady.Name = "playerStatusIndicator" + i; indicatorPlayerReady.ClientRectangle = new Rectangle(statusIndicatorX, ddPlayerTeams[i].Y + statusIndicatorY, 0, 0); PlayerOptionsPanel.AddChild(indicatorPlayerReady); StatusIndicators[i] = indicatorPlayerReady; const string spectatorName = "Spectator"; AddSideToDropDown(ddPlayerSides[i], spectatorName, spectatorName.L10N("Client:Sides:SpectatorSide"), AssetLoader.LoadTexture("spectatoricon.png")); } lbChatMessages = FindChild(nameof(lbChatMessages)); tbChatInput = FindChild(nameof(tbChatInput)); tbChatInput.MaximumTextLength = 150; tbChatInput.EnterPressed += TbChatInput_EnterPressed; btnLockGame = FindChild(nameof(btnLockGame)); btnLockGame.LeftClick += BtnLockGame_LeftClick; chkAutoReady = FindChild(nameof(chkAutoReady)); chkAutoReady.CheckedChanged += ChkAutoReady_CheckedChanged; chkAutoReady.Disable(); MapPreviewBox.LocalStartingLocationSelected += MapPreviewBox_LocalStartingLocationSelected; MapPreviewBox.StartingLocationApplied += MapPreviewBox_StartingLocationApplied; sndJoinSound = new EnhancedSoundEffect("joingame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyJoinCooldown); sndLeaveSound = new EnhancedSoundEffect("leavegame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyLeaveCooldown); sndMessageSound = new EnhancedSoundEffect("message.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundMessageCooldown); sndGetReadySound = new EnhancedSoundEffect("getready.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyGetReadyCooldown); sndReturnSound = new EnhancedSoundEffect("return.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyReturnCooldown); if (SavedGameManager.AreSavedGamesAvailable()) { fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Saved Games"), "*.NET"); fsw.Created += fsw_Created; fsw.Changed += fsw_Created; fsw.EnableRaisingEvents = false; } else { Logger.Log("MultiplayerGameLobby: Saved games are not available!"); } ParseHostPlayerControls(); } /// /// Reads INI for host/player variations of controls and restores it back. /// /// /// Needed for translation notification mechanism to work correctly. /// private void ParseHostPlayerControls() { string temp = lbChatMessages.Name; lbChatMessages.Name = "lbChatMessages_Host"; ReadINIForControl(lbChatMessages); lbChatMessages.Name = "lbChatMessages_Player"; ReadINIForControl(lbChatMessages); lbChatMessages.Name = temp; ReadINIForControl(lbChatMessages); temp = tbChatInput.Name; tbChatInput.Name = "tbChatInput_Host"; ReadINIForControl(tbChatInput); tbChatInput.Name = "tbChatInput_Player"; ReadINIForControl(tbChatInput); tbChatInput.Name = temp; ReadINIForControl(tbChatInput); } /// /// Performs initialization that is necessary after derived /// classes have performed their own initialization. /// protected void PostInitialize() { CenterOnParent(); LoadDefaultGameModeMap(); } private void fsw_Created(object sender, FileSystemEventArgs e) { AddCallback(new Action(FSWEvent), e); } private void FSWEvent(FileSystemEventArgs e) { Logger.Log("FSW Event: " + e.FullPath); if (Path.GetFileName(e.FullPath) == "SAVEGAME.NET") { if (!gameSaved) { bool success = SavedGameManager.InitSavedGames(); if (!success) return; } gameSaved = true; SavedGameManager.RenameSavedGame(); } } protected override void StartGame() { if (fsw != null) fsw.EnableRaisingEvents = true; if (UserINISettings.Instance.StopGameLobbyMessageAudio) sndMessageSound.Enabled = false; base.StartGame(); } protected override void GameProcessExited() { gameSaved = false; if (fsw != null) fsw.EnableRaisingEvents = false; PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); pInfo.IsInGame = false; if (UserINISettings.Instance.StopGameLobbyMessageAudio) sndMessageSound.Enabled = true; base.GameProcessExited(); if (IsHost) { GenerateGameID(); DdGameModeMapFilter_SelectedIndexChanged(null, EventArgs.Empty); // Refresh ranks } else if (chkAutoReady.Checked) { RequestReadyStatus(); } } private void GenerateGameID() { int i = 0; while (i < 20) { string s = DateTime.Now.Day.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Hour.ToString() + DateTime.Now.Minute.ToString(); UniqueGameID = int.Parse(i.ToString() + s); if (StatisticsManager.Instance.GetMatchWithGameID(UniqueGameID) == null) break; i++; } } private void BtnLockGame_LeftClick(object sender, EventArgs e) { HandleLockGameButtonClick(); } protected virtual void HandleLockGameButtonClick() { if (Locked) UnlockGame(true); else LockGame(); } protected abstract void LockGame(); protected abstract void UnlockGame(bool manual); private void TbChatInput_EnterPressed(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbChatInput.Text)) return; if (tbChatInput.Text.StartsWith("/")) { string text = tbChatInput.Text; string command; string parameters; int spaceIndex = text.IndexOf(' '); if (spaceIndex == -1) { command = text.Substring(1).ToUpper(); parameters = string.Empty; } else { command = text.Substring(1, spaceIndex - 1); parameters = text.Substring(spaceIndex + 1); } tbChatInput.Text = string.Empty; foreach (var chatBoxCommand in chatBoxCommands) { if (command.ToUpper() == chatBoxCommand.Command) { if (!IsHost && chatBoxCommand.HostOnly) { AddNotice(string.Format("/{0} is for game hosts only.".L10N("Client:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); return; } chatBoxCommand.Action(parameters); return; } } StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("Client:Main:ChatboxCommandTipText") + " "); foreach (var chatBoxCommand in chatBoxCommands) { sb.Append(Environment.NewLine); sb.Append(Environment.NewLine); sb.Append($"{chatBoxCommand.Command}: {chatBoxCommand.Description}"); } XNAMessageBox.Show(WindowManager, "Chat Box Command Help".L10N("Client:Main:ChatboxCommandTipTitle"), sb.ToString()); return; } SendChatMessage(tbChatInput.Text); tbChatInput.Text = string.Empty; } private void ChkAutoReady_CheckedChanged(object sender, EventArgs e) { UpdateLaunchGameButtonStatus(); RequestReadyStatus(); } protected void ResetAutoReadyCheckbox() { chkAutoReady.CheckedChanged -= ChkAutoReady_CheckedChanged; chkAutoReady.Checked = false; chkAutoReady.CheckedChanged += ChkAutoReady_CheckedChanged; UpdateLaunchGameButtonStatus(); } private void SetFrameSendRate(string value) { bool success = int.TryParse(value, out int intValue); if (!success) { AddNotice("Command syntax: /FrameSendRate ".L10N("Client:Main:ChatboxCommandFrameSendRateSyntax")); return; } FrameSendRate = intValue; AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("Client:Main:FrameSendRateChanged"), intValue)); OnGameOptionChanged(); ClearReadyStatuses(); } private void SetMaxAhead(string value) { bool success = int.TryParse(value, out int intValue); if (!success) { AddNotice("Command syntax: /MaxAhead ".L10N("Client:Main:ChatboxCommandMaxAheadSyntax")); return; } MaxAhead = intValue; AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("Client:Main:MaxAheadChanged"), intValue)); OnGameOptionChanged(); ClearReadyStatuses(); } private void SetProtocolVersion(string value) { bool success = int.TryParse(value, out int intValue); if (!success) { AddNotice("Command syntax: /ProtocolVersion .".L10N("Client:Main:ChatboxCommandProtocolVersionSyntax")); return; } if (!(intValue == 0 || intValue == 2)) { AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("Client:Main:ChatboxCommandProtocolVersionInvalid")); return; } ProtocolVersion = intValue; AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("Client:Main:ProtocolVersionChanged"), intValue)); OnGameOptionChanged(); ClearReadyStatuses(); } private void SetStartingLocationClearance(string value) { bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations); SetRandomStartingLocations(removeStartingLocations); OnGameOptionChanged(); ClearReadyStatuses(); } /// /// Enables or disables completely random starting locations and informs /// the user accordingly. /// /// The new value of completely random starting locations. protected void SetRandomStartingLocations(bool newValue) { if (newValue != RemoveStartingLocations) { RemoveStartingLocations = newValue; if (RemoveStartingLocations) AddNotice("The game host has enabled completely random starting locations (only works for regular maps).".L10N("Client:Main:HostEnabledRandomStartLocation")); else AddNotice("The game host has disabled completely random starting locations.".L10N("Client:Main:HostDisabledRandomStartLocation")); } } /// /// Handles the dice rolling command. /// /// The parameters given for the command by the user. private void RollDiceCommand(string dieType) { int dieSides = 6; int dieCount = 1; if (!string.IsNullOrEmpty(dieType)) { string[] parts = dieType.Split('d'); if (parts.Length == 2) { if (!int.TryParse(parts[0], out dieCount) || !int.TryParse(parts[1], out dieSides)) { AddNotice("Invalid dice specified. Expected format: /roll d".L10N("Client:Main:ChatboxCommandRollInvalidAndSyntax")); return; } } } if (dieCount > MAX_DICE || dieCount < 1) { AddNotice("You can only between 1 to 10 dies at once.".L10N("Client:Main:ChatboxCommandRollInvalid2")); return; } if (dieSides > MAX_DIE_SIDES || dieSides < 2) { AddNotice("You can only have between 2 and 100 sides in a die.".L10N("Client:Main:ChatboxCommandRollInvalid3")); return; } int[] results = new int[dieCount]; for (int i = 0; i < dieCount; i++) { results[i] = random.Next(1, dieSides + 1); } BroadcastDiceRoll(dieSides, results); } /// /// Handles custom map load command. /// /// Name of the map given as a parameter, without file extension. private void LoadCustomMap(string mapName) { Map map = MapLoader.LoadCustomMap($"Maps/Custom/{mapName}", out string resultMessage); if (map != null) { AddNotice(resultMessage); ListMaps(); } else { AddNotice(resultMessage, Color.Red); } } /// /// Override in derived classes to broadcast the results of rolling dice to other players. /// /// The number of sides in the dice. /// The results of the dice roll. protected abstract void BroadcastDiceRoll(int dieSides, int[] results); /// /// Parses and lists the results of rolling dice. /// /// The player that rolled the dice. /// The results of rolling dice, with each die separated by a comma /// and the number of sides in the die included as the first number. /// /// HandleDiceRollResult("Rampastring", "6,3,5,1") would mean that /// Rampastring rolled three six-sided dice and got 3, 5 and 1. /// protected void HandleDiceRollResult(string senderName, string result) { if (string.IsNullOrEmpty(result)) return; string[] parts = result.Split(','); if (parts.Length < 2 || parts.Length > MAX_DICE + 1) return; int[] intArray = Array.ConvertAll(parts, (s) => { return Conversions.IntFromString(s, -1); }); int dieSides = intArray[0]; if (dieSides < 1 || dieSides > MAX_DIE_SIDES) return; int[] results = new int[intArray.Length - 1]; Array.ConstrainedCopy(intArray, 1, results, 0, results.Length); for (int i = 1; i < intArray.Length; i++) { if (intArray[i] < 1 || intArray[i] > dieSides) return; } PrintDiceRollResult(senderName, dieSides, results); } /// /// Prints the result of rolling dice. /// /// The player who rolled dice. /// The number of sides in the die. /// The results of the roll. protected void PrintDiceRollResult(string senderName, int dieSides, int[] results) { AddNotice(String.Format("{0} rolled {1}d{2} and got {3}".L10N("Client:Main:PrintDiceRollResult"), senderName, results.Length, dieSides, string.Join(", ", results) )); } protected abstract void SendChatMessage(string message); /// /// Changes the game lobby's UI depending on whether the local player is the host. /// /// Determines whether the local player is the host of the game. protected void Refresh(bool isHost) { IsHost = isHost; Locked = false; CopyPlayerDataToUI(); UpdateMapPreviewBoxEnabledStatus(); PlayerExtraOptionsPanel?.SetIsHost(isHost); //MapPreviewBox.EnableContextMenu = IsHost; btnLaunchGame.Text = IsHost ? BTN_LAUNCH_GAME : BTN_LAUNCH_READY; if (IsHost) { ShowMapList(); btnSaveLoadGameOptions?.Enable(); btnLockGame.Text = "Lock Game".L10N("Client:Main:ButtonLockGame"); btnLockGame.Enabled = true; btnLockGame.Visible = true; chkAutoReady.Disable(); foreach (GameLobbyDropDown dd in DropDowns) { dd.InputEnabled = true; dd.SelectedIndex = dd.UserSelectedIndex; } foreach (GameLobbyCheckBox checkBox in CheckBoxes) { checkBox.AllowChanges = true; checkBox.Checked = checkBox.UserChecked; } GenerateGameID(); } else { HideMapList(); btnSaveLoadGameOptions?.Disable(); btnLockGame.Enabled = false; btnLockGame.Visible = false; ReadINIForControl(chkAutoReady); foreach (GameLobbyDropDown dd in DropDowns) dd.InputEnabled = false; foreach (GameLobbyCheckBox checkBox in CheckBoxes) checkBox.AllowChanges = false; } LoadDefaultGameModeMap(); lbChatMessages.Clear(); lbChatMessages.TopIndex = 0; lbChatMessages.AddItem("Type / to view a list of available chat commands.".L10N("Client:Main:ChatCommandTip"), Color.Silver, true); if (SavedGameManager.GetSaveGameCount() > 0) { lbChatMessages.AddItem(("Multiplayer saved games from a previous match have been detected. " + "The saved games of the previous match will be deleted if you create new saves during this match.").L10N("Client:Main:SavedGameDetected"), Color.Yellow, true); } } private void HideMapList() { lbChatMessages.Name = "lbChatMessages_Player"; tbChatInput.Name = "tbChatInput_Player"; MapPreviewBox.Name = "MapPreviewBox"; lblMapName.Name = "lblMapName"; lblMapAuthor.Name = "lblMapAuthor"; lblGameMode.Name = "lblGameMode"; lblMapSize.Name = "lblMapSize"; ReadINIForControl(btnPickRandomMap); ReadINIForControl(lbChatMessages); ReadINIForControl(tbChatInput); ReadINIForControl(lbGameModeMapList); ReadINIForControl(lblMapName); ReadINIForControl(lblMapAuthor); ReadINIForControl(lblGameMode); ReadINIForControl(lblMapSize); ReadINIForControl(btnMapSortAlphabetically); ddGameModeMapFilter.Disable(); lblGameModeSelect.Disable(); lbGameModeMapList.Disable(); tbMapSearch.Disable(); btnPickRandomMap.Disable(); btnMapSortAlphabetically.Disable(); SetMapLabels(); } private void ShowMapList() { lbChatMessages.Name = "lbChatMessages_Host"; tbChatInput.Name = "tbChatInput_Host"; MapPreviewBox.Name = "MapPreviewBox"; lblMapName.Name = "lblMapName"; lblMapAuthor.Name = "lblMapAuthor"; lblGameMode.Name = "lblGameMode"; lblMapSize.Name = "lblMapSize"; ddGameModeMapFilter.Enable(); lblGameModeSelect.Enable(); lbGameModeMapList.Enable(); tbMapSearch.Enable(); btnPickRandomMap.Enable(); btnMapSortAlphabetically.Enable(); ReadINIForControl(btnPickRandomMap); ReadINIForControl(lbChatMessages); ReadINIForControl(tbChatInput); ReadINIForControl(lbGameModeMapList); ReadINIForControl(lblMapName); ReadINIForControl(lblMapAuthor); ReadINIForControl(lblGameMode); ReadINIForControl(lblMapSize); ReadINIForControl(btnMapSortAlphabetically); SetMapLabels(); } private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalStartingLocationEventArgs e) { int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); if (mTopIndex == -1 || Players[mTopIndex].SideId == ddPlayerSides[0].Items.Count - 1) return; ddPlayerStarts[mTopIndex].SelectedIndex = e.StartingLocationIndex; } private void MapPreviewBox_StartingLocationApplied(object sender, EventArgs e) { ClearReadyStatuses(); CopyPlayerDataToUI(); BroadcastPlayerOptions(); } /// /// Handles the user's click on the "Launch Game" / "I'm Ready" button. /// If the local player is the game host, checks if the game can be launched and then /// launches the game if it's allowed. If the local player isn't the game host, /// sends a ready request. /// protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) { if (!IsHost) { RequestReadyStatus(); return; } if (!Locked) { LockGameNotification(); return; } var teamMappingsError = GetTeamMappingsError(); if (!string.IsNullOrEmpty(teamMappingsError)) { AddNotice(teamMappingsError); return; } List occupiedColorIds = new List(); foreach (PlayerInfo player in Players) { if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) { SharedColorsNotification(); return; } occupiedColorIds.Add(player.ColorId); } if (AIPlayers.Count(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1) > 0) { AISpectatorsNotification(); return; } if (GameModeMap.EnforceMaxPlayers) { foreach (PlayerInfo pInfo in Players) { if (pInfo.StartingLocation == 0) continue; if (Players.Concat(AIPlayers).ToList().Find( p => p.StartingLocation == pInfo.StartingLocation && p.Name != pInfo.Name) != null) { SharedStartingLocationNotification(); return; } } for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { int startingLocation = AIPlayers[aiId].StartingLocation; if (startingLocation == 0) continue; int index = AIPlayers.FindIndex(aip => aip.StartingLocation == startingLocation); if (index > -1 && index != aiId) { SharedStartingLocationNotification(); return; } } int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; int minPlayers = GameModeMap.MinPlayers; if (totalPlayerCount < minPlayers) { InsufficientPlayersNotification(); return; } if (GameModeMap.EnforceMaxPlayers && totalPlayerCount > GameModeMap.MaxPlayers) { TooManyPlayersNotification(); return; } } int iId = 0; foreach (PlayerInfo player in Players) { iId++; if (player.Name == ProgramConstants.PLAYERNAME) continue; if (!player.HashReceived) { NotVerifiedNotification(iId - 1); return; } if (player.IsInGame) { StillInGameNotification(iId - 1); return; } /* if (DisableSpectatorReadyChecking) { // Only account ready status if player is not a spectator if (!player.Ready && !IsPlayerSpectator(player)) { GetReadyNotification(); return; } } else { if (!player.Ready) { GetReadyNotification(); return; } } */ if (!player.Ready) { GetReadyNotification(); return; } } HostLaunchGame(); } protected virtual void LockGameNotification() => AddNotice("The host needs to lock the game room before launching the game.".L10N("Client:Main:LockGameNotificationV2")); protected virtual void SharedColorsNotification() => AddNotice("Multiple human players cannot share the same color.".L10N("Client:Main:SharedColorsNotification")); protected virtual void AISpectatorsNotification() => AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("Client:Main:AISpectatorsNotification")); protected virtual void SharedStartingLocationNotification() => AddNotice("Multiple players cannot share the same starting location on this map.".L10N("Client:Main:SharedStartingLocationNotification")); protected virtual void NotVerifiedNotification(int playerIndex) { if (playerIndex > -1 && playerIndex < Players.Count) AddNotice(string.Format("Unable to launch game. Player {0} hasn't been verified.".L10N("Client:Main:NotVerifiedNotification"), Players[playerIndex].Name)); } protected virtual void StillInGameNotification(int playerIndex) { if (playerIndex > -1 && playerIndex < Players.Count) { AddNotice(String.Format("Unable to launch game. Player {0} is still playing the game you started previously.".L10N("Client:Main:StillInGameNotification"), Players[playerIndex].Name)); } } protected virtual void GetReadyNotification() { AddNotice("The host wants to start the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyNotification")); if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) sndGetReadySound.Play(); } protected virtual void InsufficientPlayersNotification() { Debug.Assert(GameModeMap != null, "GameModeMap should not be null"); AddNotice(string.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotificationV2"), GameModeMap.ToString(), GameModeMap.MinPlayers)); } protected virtual void TooManyPlayersNotification() { Debug.Assert(GameModeMap != null, "GameModeMap should not be null"); AddNotice(string.Format("Unable to launch game: {0} cannot be played with more than {1} players.".L10N("Client:Main:TooManyPlayersNotificationV2"), GameModeMap.ToString(), GameModeMap.MaxPlayers)); } public virtual void Clear() { if (!IsHost) AIPlayers.Clear(); Players.Clear(); } protected override void OnGameOptionChanged() { base.OnGameOptionChanged(); ClearReadyStatuses(); CopyPlayerDataToUI(); } protected abstract void HostLaunchGame(); protected override void CopyPlayerDataFromUI(object sender, EventArgs e) { if (PlayerUpdatingInProgress) return; if (IsHost) { base.CopyPlayerDataFromUI(sender, e); BroadcastPlayerOptions(); return; } int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); if (mTopIndex == -1) return; int requestedSide = ddPlayerSides[mTopIndex].SelectedIndex; int requestedColor = ddPlayerColors[mTopIndex].SelectedIndex; int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex; int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex; RequestPlayerOptions(requestedSide, requestedColor, requestedStart, requestedTeam); } protected override void CopyPlayerDataToUI() { if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT) return; base.CopyPlayerDataToUI(); ClearPingIndicators(); if (IsHost) { for (int pId = 1; pId < Players.Count; pId++) ddPlayerNames[pId].AllowDropDown = true; } // Player statuses for (int pId = 0; pId < Players.Count; pId++) { /* if (pId != 0 && !Players[pId].HashReceived) // If player is not verified (not counting the host) { StatusIndicators[pId].SwitchTexture("error"); } else */ if (Players[pId].IsInGame) // If player is ingame { StatusIndicators[pId].SwitchTexture(PlayerSlotState.InGame); } else if (pId == 0) // If player is host { StatusIndicators[pId].SwitchTexture(Locked ? PlayerSlotState.Ready : PlayerSlotState.NotReady); // Display room lock } else { // StatusIndicators[pId].SwitchTexture( // (IsPlayerSpectator(Players[pId]) && DisableSpectatorReadyChecking) // ? "okDisabled" : "ok"); StatusIndicators[pId].SwitchTexture(Players[pId].Ready ? PlayerSlotState.Ready : PlayerSlotState.NotReady); } /* else { // StatusIndicators[pId].SwitchTexture( // (IsPlayerSpectator(Players[pId]) && DisableSpectatorReadyChecking) // ? "offDisabled" : "off"); } */ UpdatePlayerPingIndicator(Players[pId]); } // AI statuses for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { StatusIndicators[aiId + Players.Count].SwitchTexture( IsPlayerSpectator(AIPlayers[aiId]) ? PlayerSlotState.Error : PlayerSlotState.AI); if (IsPlayerSpectator(AIPlayers[aiId])) StatusIndicators[aiId + Players.Count].ToolTip.Text += Environment.NewLine + "AI players can't be spectators.".L10N("Client:ClientGUI:AICantSpec"); } // Empty slot statuses for (int i = AIPlayers.Count + Players.Count; i < MAX_PLAYER_COUNT; i++) { StatusIndicators[i].SwitchTexture(PlayerSlotState.Empty); } } protected virtual void ClearPingIndicators() { foreach (XNAClientDropDown dd in ddPlayerNames) { dd.Items[0].Texture = null; dd.ToolTip.Text = string.Empty; } } protected virtual void UpdatePlayerPingIndicator(PlayerInfo pInfo) { XNAClientDropDown ddPlayerName = ddPlayerNames[pInfo.Index]; ddPlayerName.Items[0].Texture = GetTextureForPing(pInfo.Ping); if (pInfo.Ping < 0) ddPlayerName.ToolTip.Text = "Ping:".L10N("Client:Main:PlayerInfoPing") + " ? " + "ms".L10N("Client:Main:MillisecondsShort"); else ddPlayerName.ToolTip.Text = "Ping:".L10N("Client:Main:PlayerInfoPing") + $" {pInfo.Ping} " + "ms".L10N("Client:Main:MillisecondsShort"); } private Texture2D GetTextureForPing(int ping) { switch (ping) { case int p when (p > 350): return PingTextures[4]; case int p when (p > 250): return PingTextures[3]; case int p when (p > 100): return PingTextures[2]; case int p when (p >= 0): return PingTextures[1]; default: return PingTextures[0]; } } protected abstract void BroadcastPlayerOptions(); protected abstract void BroadcastPlayerExtraOptions(); protected abstract void RequestPlayerOptions(int side, int color, int start, int team); protected abstract void RequestReadyStatus(); // this public as it is used by the main lobby to notify the user of invitation failure public void AddWarning(string message) { AddNotice(message, Color.Yellow); } protected override bool AllowPlayerOptionsChange() => IsHost; protected override void ChangeMap(GameModeMap gameModeMap) { base.ChangeMap(gameModeMap); bool resetAutoReady = gameModeMap?.GameMode == null || gameModeMap?.Map == null; ClearReadyStatuses(resetAutoReady); if ((LastMapChangeWasInvalid || resetAutoReady) && chkAutoReady.Checked) RequestReadyStatus(); LastMapChangeWasInvalid = resetAutoReady; //if (IsHost) // OnGameOptionChanged(); } protected override void ToggleFavoriteMap() { base.ToggleFavoriteMap(); if ((GameModeMap != null && GameModeMap.IsFavorite) || !IsHost) return; RefreshForFavoriteMapRemoved(); } protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); iniFile.SetIntValue("Settings", "FrameSendRate", FrameSendRate); if (MaxAhead > 0) iniFile.SetIntValue("Settings", "MaxAhead", MaxAhead); iniFile.SetIntValue("Settings", "Protocol", ProtocolVersion); } protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) { if (gameModeMap.MaxPlayers > 3) return StatisticsManager.Instance.GetCoopRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.MaxPlayers); if (StatisticsManager.Instance.HasWonMapInPvP(gameModeMap.Map.UntranslatedName, gameModeMap.GameMode.UntranslatedUIName, gameModeMap.MaxPlayers)) return 2; return -1; } public void SwitchOn() => Enable(); public void SwitchOff() => Disable(); public abstract string GetSwitchName(); protected override void UpdateMapPreviewBoxEnabledStatus() { if (Map != null && GameMode != null) { bool disablestartlocs = GameModeMap.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts; MapPreviewBox.EnableContextMenu = disablestartlocs ? false : IsHost; MapPreviewBox.EnableStartLocationSelection = !disablestartlocs; } else { MapPreviewBox.EnableContextMenu = IsHost; MapPreviewBox.EnableStartLocationSelection = true; } } protected override bool UpdateLaunchGameButtonStatus() { if (IsHost) btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && GameMode != null && Map != null; else btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && !chkAutoReady.Checked; return btnLaunchGame.Enabled; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/PlayerLocationIndicator.cs ================================================ using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; using System.Linq; using ClientCore; using Rampastring.XNAUI; using Microsoft.Xna.Framework.Graphics; using PlayerInfo = DTAClient.Domain.Multiplayer.PlayerInfo; using Microsoft.Xna.Framework; using DTAClient.Domain.Multiplayer; namespace DTAClient.DXGUI.Multiplayer.GameLobby { /// /// A player location indicator for the map preview. /// public class PlayerLocationIndicator : XNAControl { const float TEXTURE_SCALE = 0.25f; public PlayerLocationIndicator(WindowManager windowManager, List mpColors, Color nameBackgroundColor, Color nameBorderColor, XNAContextMenu contextMenu) : base(windowManager) { this.mpColors = mpColors; this.nameBackgroundColor = nameBackgroundColor; this.nameBorderColor = nameBorderColor; this.contextMenu = contextMenu; HoverRemapColor = Color.White; usePlayerRemapColor = ClientConfiguration.Instance.MapPreviewStartingLocationUsePlayerRemapColor; } private Texture2D baseTexture; private Texture2D hoverTexture; private Texture2D usedTexture; public Texture2D WaypointTexture { get; set; } public List Players = new List(); List mpColors; public bool BackgroundShown { get; set; } public int FontIndex { get; set; } public double AngularVelocity = 0.015; public double ReversedAngularVelocity = -0.0075; public Color HoverRemapColor { get; set; } private XNAContextMenu contextMenu { get; set; } private Color nameBackgroundColor; private Color nameBorderColor; private readonly string[] teamIds = new[] { string.Empty } .Concat(ProgramConstants.TEAMS.Select(team => $"[{team}]")).ToArray(); private bool usePlayerRemapColor = false; private bool isHoveredOn = false; private double backgroundAlpha = 0.0; private double backgroundAlphaRate = 0.1; private double angle; private int lineHeight; private Vector2 textSize; private int textXPosition; private List pText = new List(); public override void Initialize() { base.Initialize(); baseTexture = AssetLoader.LoadTexture("slocindicator.png"); hoverTexture = AssetLoader.LoadTexture("slocindicatorh.png"); ClientRectangle = baseTexture.Bounds; lineHeight = (int)Renderer.GetTextDimensions("@", FontIndex).Y + 1; usedTexture = baseTexture; } public void SetPosition(Point p) { int width = (int)(baseTexture.Width * TEXTURE_SCALE); int height = (int)(baseTexture.Height * TEXTURE_SCALE); ClientRectangle = new Rectangle(p.X - width / 2, p.Y - height / 2, width, height); } public void Refresh() { textSize = Vector2.Zero; pText.Clear(); foreach (PlayerInfo pInfo in Players) { string text = pInfo.Name; if (pInfo.TeamId > 0) text = teamIds[pInfo.TeamId] + " " + pInfo.Name; if (text == null) return; Vector2 pInfoSize = Renderer.GetTextDimensions(text, FontIndex); if (pInfoSize.X > textSize.X) textSize = new Vector2(pInfoSize.X, Players.Count * (pInfoSize.Y + 1)); textXPosition = 3; bool textOnRight = true; if (Right + textXPosition + (int)textSize.X > Parent.Width) { textXPosition = -(int)textSize.X - 3 - (int)(baseTexture.Width * TEXTURE_SCALE); text = pInfo.TeamId > 0 ? pInfo.Name + " " + teamIds[pInfo.TeamId] : pInfo.Name; textOnRight = false; } pText.Add(new PlayerText(text, textOnRight)); } } protected override void OnVisibleChanged(object sender, EventArgs args) { base.OnVisibleChanged(sender, args); backgroundAlpha = 0.0; } public override void OnMouseEnter() { //usedTexture = hoverTexture; isHoveredOn = true; base.OnMouseEnter(); } public override void OnMouseLeave() { //usedTexture = baseTexture; isHoveredOn = false; base.OnMouseLeave(); } public override void Update(GameTime gameTime) { base.Update(gameTime); double frameTimeCoefficient = gameTime.ElapsedGameTime.TotalMilliseconds / 10.0; angle += Players.Count > 0 ? ReversedAngularVelocity * frameTimeCoefficient : AngularVelocity * frameTimeCoefficient; if (Players.Count > 0) { usedTexture = hoverTexture; } else usedTexture = baseTexture; if (BackgroundShown) backgroundAlpha = Math.Min(backgroundAlpha + backgroundAlphaRate, 1.0); else backgroundAlpha = Math.Max(backgroundAlpha - backgroundAlphaRate, 0.0); } public override void Draw(GameTime gameTime) { Point p = GetWindowPoint(); Rectangle displayRectangle = new Rectangle(p.X, p.Y, Width, Height); int y = displayRectangle.Y + ((int)(baseTexture.Height * TEXTURE_SCALE) - lineHeight) / 2; int i = 0; foreach (PlayerInfo pInfo in Players) { Color textColor = Color.White; if (pInfo.ColorId > 0) textColor = mpColors[pInfo.ColorId - 1].XnaColor; if (backgroundAlpha > 0.0) { int rectangleWidth = 0; int rectangleCoordX = 0; if (pText[i].TextOnRight) { rectangleCoordX = displayRectangle.Center.X; rectangleWidth = (int)textSize.X + textXPosition + displayRectangle.Width / 2 + 5; } else { rectangleWidth = (int)textSize.X + displayRectangle.Width / 2 + 5; rectangleCoordX = displayRectangle.Center.X - rectangleWidth; } Renderer.FillRectangle(new Rectangle(rectangleCoordX, y, rectangleWidth, lineHeight), new Color(nameBackgroundColor.R, nameBackgroundColor.G, nameBackgroundColor.B, (int)(nameBackgroundColor.A * backgroundAlpha))); Renderer.DrawRectangle(new Rectangle(rectangleCoordX, y, rectangleWidth, lineHeight), new Color(nameBorderColor.R, nameBorderColor.G, nameBorderColor.B, (int)(nameBorderColor.A * backgroundAlpha))); } Renderer.DrawStringWithShadow(pText[i].Text, FontIndex, new Vector2(displayRectangle.Right + textXPosition, y), textColor); y += lineHeight; i++; } Vector2 origin = new Vector2(usedTexture.Width / 2, usedTexture.Height / 2); Renderer.DrawTexture(usedTexture, new Vector2(displayRectangle.Center.X + 1.5f, displayRectangle.Center.Y + 1f), (float)angle, origin, new Vector2(TEXTURE_SCALE), Color.Black); Color remapColor = Color.White; Color hoverRemapColor = HoverRemapColor; if (Players.Count == 1 && Players[0].ColorId > 0) { remapColor = mpColors[Players[0].ColorId - 1].XnaColor; hoverRemapColor = remapColor; } if (isHoveredOn || (contextMenu.Tag == this.Tag && contextMenu.Visible)) { Renderer.DrawTexture(usedTexture, new Vector2(displayRectangle.Center.X + 0.5f, displayRectangle.Center.Y), (float)angle, origin, new Vector2(TEXTURE_SCALE + 0.1f), hoverRemapColor); } Renderer.DrawTexture(usedTexture, new Vector2(displayRectangle.Center.X + 0.5f, displayRectangle.Center.Y), (float)angle, origin, new Vector2(TEXTURE_SCALE), remapColor); if (WaypointTexture != null) { // Non-premultiplied blending makes the indicators look sharper for some reason // TODO figure out why Renderer.PushSettings(new SpriteBatchSettings(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, null, null, null)); Renderer.DrawTexture(WaypointTexture, new Vector2(displayRectangle.Center.X + 0.5f, displayRectangle.Center.Y), 0f, new Vector2(WaypointTexture.Width / 2, WaypointTexture.Height / 2), new Vector2(1f, 1f), Color.White); Renderer.PopSettings(); } base.Draw(gameTime); } sealed class PlayerText { public PlayerText(string text, bool textOnRight) { Text = text; TextOnRight = textOnRight; } public string Text { get; set; } public bool TextOnRight { get; set; } } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using ClientCore; using ClientCore.Extensions; using ClientCore.Statistics; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.Multiplayer; using DTAClient.DXGUI.Generic; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class SkirmishLobby : GameLobbyBase, ISwitchable { private const string SETTINGS_PATH = "Client/SkirmishSettings.ini"; public SkirmishLobby(WindowManager windowManager, TopBar topBar, MapLoader mapLoader, DiscordHandler discordHandler, Random random) : base(windowManager, "SkirmishLobby", mapLoader, false, discordHandler, random) { this.topBar = topBar; this.random = random; } public event EventHandler Exited; private Random random; TopBar topBar; public override void Initialize() { base.Initialize(); RandomSeed = random.Next(); //InitPlayerOptionDropdowns(128, 98, 90, 48, 55, new Point(6, 24)); InitPlayerOptionDropdowns(); btnLeaveGame.Text = "Main Menu".L10N("Client:Main:MainMenu"); //MapPreviewBox.EnableContextMenu = true; const string spectatorName = "Spectator"; AddSideToDropDown(ddPlayerSides[0], spectatorName, spectatorName.L10N("Client:Sides:SpectatorSide"), AssetLoader.LoadTexture("spectatoricon.png")); MapPreviewBox.LocalStartingLocationSelected += MapPreviewBox_LocalStartingLocationSelected; MapPreviewBox.StartingLocationApplied += MapPreviewBox_StartingLocationApplied; WindowManager.CenterControlOnScreen(this); LoadSettings(); CopyPlayerDataToUI(); ProgramConstants.PlayerNameChanged += ProgramConstants_PlayerNameChanged; ddPlayerSides[0].SelectedIndexChanged += PlayerSideChanged; PlayerExtraOptionsPanel?.SetIsHost(true); } protected override void ToggleFavoriteMap() { base.ToggleFavoriteMap(); if (GameModeMap != null && GameModeMap.IsFavorite) return; RefreshForFavoriteMapRemoved(); } protected override void AddNotice(string message, Color color) { XNAMessageBox.Show(WindowManager, "Message".L10N("Client:Main:MessageTitle"), message); } protected override void OnEnabledChanged(object sender, EventArgs args) { base.OnEnabledChanged(sender, args); if (Enabled) UpdateDiscordPresence(true); else ResetDiscordPresence(); } private void ProgramConstants_PlayerNameChanged(object sender, EventArgs e) { Players[0].Name = ProgramConstants.PLAYERNAME; CopyPlayerDataToUI(); } private void MapPreviewBox_StartingLocationApplied(object sender, EventArgs e) { CopyPlayerDataToUI(); } private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalStartingLocationEventArgs e) { Players[0].StartingLocation = e.StartingLocationIndex + 1; CopyPlayerDataToUI(); } private string CheckGameValidity() { int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; if (GameModeMap.MultiplayerOnly) { return string.Format("{0} can only be played on CnCNet and LAN.".L10N("Client:Main:GameModeMultiplayerOnly"), GameModeMap.ToString()); } if (totalPlayerCount < GameModeMap.MinPlayers) { return string.Format("{0} cannot be played with less than {1} players.".L10N("Client:Main:GameModeInsufficientPlayers"), GameModeMap.ToString(), GameModeMap.MinPlayers); } if (GameModeMap.EnforceMaxPlayers) { if (totalPlayerCount > GameModeMap.MaxPlayers) { return string.Format("{0} cannot be played with more than {1} players.".L10N("Client:Main:TooManyPlayers"), GameModeMap.ToString(), GameModeMap.MaxPlayers); } IEnumerable concatList = Players.Concat(AIPlayers); foreach (PlayerInfo pInfo in concatList) { if (pInfo.StartingLocation == 0) continue; if (concatList.Count(p => p.StartingLocation == pInfo.StartingLocation) > 1) { return "Multiple players cannot share the same starting location on the selected map.".L10N("Client:Main:StartLocationOccupied"); } } } if (GameModeMap.IsCoop && Players[0].SideId == ddPlayerSides[0].Items.Count - 1) { return "Co-op missions cannot be spectated. You'll have to show a bit more effort to cheat here.".L10N("Client:Main:CoOpMissionSpectatorPrompt"); } var teamMappingsError = GetTeamMappingsError(); if (!string.IsNullOrEmpty(teamMappingsError)) return teamMappingsError; return null; } protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) { string error = CheckGameValidity(); if (error == null) { SaveSettings(); StartGame(); return; } XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("Client:Main:LaunchGameErrorTitle"), error); } protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) { Exited?.Invoke(this, EventArgs.Empty); PlayerExtraOptionsPanel?.Disable(); Disable(); topBar.RemovePrimarySwitchable(this); ResetDiscordPresence(); } private void PlayerSideChanged(object sender, EventArgs e) { UpdateDiscordPresence(); } protected override void UpdateDiscordPresence(bool resetTimer = false) { if (discordHandler == null || Map == null || GameMode == null || !Initialized) return; int playerIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); if (playerIndex >= MAX_PLAYER_COUNT || playerIndex < 0) return; XNAClientDropDown sideDropDown = ddPlayerSides[playerIndex]; if (sideDropDown.SelectedItem == null) return; string side = (string)sideDropDown.SelectedItem.Tag; string currentState = ProgramConstants.IsInGame ? "In Game" : "Setting Up"; discordHandler.UpdatePresence( Map.UntranslatedName, GameMode.UntranslatedUIName, currentState, side, resetTimer); } protected override bool AllowPlayerOptionsChange() { return true; } protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) { return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.MaxPlayers); } protected override void GameProcessExited() { base.GameProcessExited(); DdGameModeMapFilter_SelectedIndexChanged(null, EventArgs.Empty); // Refresh ranks RandomSeed = random.Next(); } public void Open() { topBar.AddPrimarySwitchable(this); Enable(); } public void SwitchOn() { Enable(); } public void SwitchOff() { Disable(); } public string GetSwitchName() { return "Skirmish Lobby".L10N("Client:Main:SkirmishLobby"); } /// /// Saves skirmish settings to an INI file on the file system. /// private void SaveSettings() { try { FileInfo settingsFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH); // Delete the file so we don't keep potential extra AI players that already exist in the file settingsFileInfo.Delete(); var skirmishSettingsIni = new IniFile(settingsFileInfo.FullName); skirmishSettingsIni.SetStringValue("Player", "Info", Players[0].ToString()); for (int i = 0; i < AIPlayers.Count; i++) { skirmishSettingsIni.SetStringValue("AIPlayers", i.ToString(), AIPlayers[i].ToString()); } skirmishSettingsIni.SetStringValue("Settings", "Map", Map?.SHA1 ?? string.Empty); skirmishSettingsIni.SetStringValue("Settings", "GameModeMapFilter", ddGameModeMapFilter.SelectedItem?.Text); if (ClientConfiguration.Instance.SaveSkirmishGameOptions) { foreach (GameLobbyDropDown dd in DropDowns) { skirmishSettingsIni.SetStringValue("GameOptions", dd.Name, dd.UserSelectedIndex + ""); } foreach (GameLobbyCheckBox cb in CheckBoxes) { skirmishSettingsIni.SetStringValue("GameOptions", cb.Name, cb.Checked.ToString()); } } skirmishSettingsIni.WriteIniFile(); } catch (Exception ex) { Logger.Log("Saving skirmish settings failed! Reason: " + ex.ToString()); #if DEBUG Debugger.Break(); #endif } } /// /// Loads skirmish settings from an INI file on the file system. /// private void LoadSettings() { if (!SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH).Exists) { InitDefaultSettings(); return; } var skirmishSettingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, SETTINGS_PATH)); string gameModeMapFilterName = skirmishSettingsIni.GetStringValue("Settings", "GameModeMapFilter", string.Empty); if (string.IsNullOrEmpty(gameModeMapFilterName)) gameModeMapFilterName = skirmishSettingsIni.GetStringValue("Settings", "GameMode", string.Empty); // legacy var gameModeMapFilter = ddGameModeMapFilter.Items.Find(i => i.Text == gameModeMapFilterName)?.Tag as GameModeMapFilter; if (gameModeMapFilter == null || !gameModeMapFilter.Any()) gameModeMapFilter = GetDefaultGameModeMapFilter(); var gameModeMap = gameModeMapFilter.GetGameModeMaps().FirstOrDefault(); if (gameModeMap != null) { GameModeMap = gameModeMap; ddGameModeMapFilter.SelectedIndex = ddGameModeMapFilter.Items.FindIndex(i => i.Tag == gameModeMapFilter); string mapSHA1 = skirmishSettingsIni.GetStringValue("Settings", "Map", string.Empty); int gameModeMapIndex = GetSortedGameModeMaps().FindIndex(gmm => gmm.Map.SHA1 == mapSHA1); if (gameModeMapIndex > -1) { lbGameModeMapList.SelectedIndex = gameModeMapIndex; while (gameModeMapIndex > lbGameModeMapList.LastIndex) lbGameModeMapList.TopIndex++; } } else LoadDefaultGameModeMap(); var player = PlayerInfo.FromString(skirmishSettingsIni.GetStringValue("Player", "Info", string.Empty)); if (player == null) { Logger.Log("Failed to load human player information from skirmish settings!"); InitDefaultSettings(); return; } CheckLoadedPlayerVariableBounds(player); player.Name = ProgramConstants.PLAYERNAME; Players.Add(player); List keys = skirmishSettingsIni.GetSectionKeys("AIPlayers"); if (keys == null) { keys = new List(); // No point skip parsing all settings if only AI info is missing. //Logger.Log("AI player information doesn't exist in skirmish settings!"); //InitDefaultSettings(); //return; } bool AIAllowed = GameModeMap != null && !GameModeMap.HumanPlayersOnly; foreach (string key in keys) { if (!AIAllowed) break; var aiPlayer = PlayerInfo.FromString(skirmishSettingsIni.GetStringValue("AIPlayers", key, string.Empty)); CheckLoadedPlayerVariableBounds(aiPlayer, true); if (aiPlayer == null) { Logger.Log("Failed to load AI player information from skirmish settings!"); InitDefaultSettings(); return; } if (AIPlayers.Count < MAX_PLAYER_COUNT - 1) AIPlayers.Add(aiPlayer); } if (ClientConfiguration.Instance.SaveSkirmishGameOptions) { foreach (GameLobbyDropDown dd in DropDowns) { // Maybe we should build an union of the game mode and map // forced options, we'd have less repetitive code that way if (GameMode != null) { int gameModeMatchIndex = GameMode.ForcedDropDownValues.FindIndex(p => p.Key.Equals(dd.Name)); if (gameModeMatchIndex > -1) { Logger.Log("Dropdown '" + dd.Name + "' has forced value in gamemode - saved settings ignored."); continue; } } if (Map != null) { int gameModeMatchIndex = Map.ForcedDropDownValues.FindIndex(p => p.Key.Equals(dd.Name)); if (gameModeMatchIndex > -1) { Logger.Log("Dropdown '" + dd.Name + "' has forced value in map - saved settings ignored."); continue; } } dd.UserSelectedIndex = skirmishSettingsIni.GetIntValue("GameOptions", dd.Name, dd.UserSelectedIndex); if (dd.UserSelectedIndex > -1 && dd.UserSelectedIndex < dd.Items.Count) dd.SelectedIndex = dd.UserSelectedIndex; } foreach (GameLobbyCheckBox cb in CheckBoxes) { if (GameMode != null) { int gameModeMatchIndex = GameMode.ForcedCheckBoxValues.FindIndex(p => p.Key.Equals(cb.Name)); if (gameModeMatchIndex > -1) { Logger.Log("Checkbox '" + cb.Name + "' has forced value in gamemode - saved settings ignored."); continue; } } if (Map != null) { int gameModeMatchIndex = Map.ForcedCheckBoxValues.FindIndex(p => p.Key.Equals(cb.Name)); if (gameModeMatchIndex > -1) { Logger.Log("Checkbox '" + cb.Name + "' has forced value in map - saved settings ignored."); continue; } } cb.Checked = skirmishSettingsIni.GetBooleanValue("GameOptions", cb.Name, cb.Checked); } } } /// /// Checks that a player's color, team and starting location /// don't exceed allowed bounds. /// /// The PlayerInfo. private void CheckLoadedPlayerVariableBounds(PlayerInfo pInfo, bool isAIPlayer = false) { int sideCount = SideCount + RandomSelectorCount; if (isAIPlayer) sideCount--; if (pInfo.SideId < 0 || pInfo.SideId > sideCount) { pInfo.SideId = 0; } if (pInfo.ColorId < 0 || pInfo.ColorId > MPColors.Count) { pInfo.ColorId = 0; } if (pInfo.TeamId < 0 || pInfo.TeamId >= ddPlayerTeams[0].Items.Count || (!(GameModeMap?.IsCoop ?? false)) && (GameModeMap?.ForceNoTeams ?? false)) { pInfo.TeamId = 0; } if (pInfo.StartingLocation < 0 || pInfo.StartingLocation > MAX_PLAYER_COUNT || (GameModeMap?.ForceRandomStartLocations ?? false)) { pInfo.StartingLocation = 0; } } private void InitDefaultSettings() { Players.Clear(); AIPlayers.Clear(); Players.Add(new PlayerInfo(ProgramConstants.PLAYERNAME, 0, 0, 0, 0)); PlayerInfo aiPlayer = new PlayerInfo(ProgramConstants.AI_PLAYER_NAMES[0], 0, 0, 0, 0); aiPlayer.IsAI = true; aiPlayer.AILevel = 0; AIPlayers.Add(aiPlayer); LoadDefaultGameModeMap(); } protected override void UpdateMapPreviewBoxEnabledStatus() { MapPreviewBox.EnableContextMenu = GameModeMap != null && !(GameModeMap.ForceRandomStartLocations || GetPlayerExtraOptions().IsForceRandomStarts); MapPreviewBox.EnableStartLocationSelection = MapPreviewBox.EnableContextMenu; } protected override bool UpdateLaunchGameButtonStatus() { btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && GameMode != null && Map != null; return btnLaunchGame.Enabled; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANGameCreationWindow.cs ================================================ using ClientGUI; using System; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using Microsoft.Xna.Framework; using ClientCore; using System.IO; using Rampastring.Tools; using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer { /// /// A window that makes it possible for a LAN player who's hosting a game /// to pick between hosting a new game and hosting a loaded game. /// class LANGameCreationWindow : XNAWindow { public LANGameCreationWindow(WindowManager windowManager) : base(windowManager) { } public event EventHandler NewGame; public event EventHandler LoadGame; private XNALabel lblDescription; private XNAButton btnNewGame; private XNAButton btnLoadGame; private XNAButton btnCancel; public override void Initialize() { Name = "LANGameCreationWindow"; BackgroundTexture = AssetLoader.LoadTexture("gamecreationoptionsbg.png"); ClientRectangle = new Rectangle(0, 0, 447, 77); lblDescription = new XNALabel(WindowManager); lblDescription.Name = "lblDescription"; lblDescription.FontIndex = 1; lblDescription.Text = "SELECT SESSION TYPE".L10N("Client:Main:SelectMissionType"); AddChild(lblDescription); lblDescription.CenterOnParent(); lblDescription.ClientRectangle = new Rectangle( lblDescription.X, 12, lblDescription.Width, lblDescription.Height); btnNewGame = new XNAButton(WindowManager); btnNewGame.Name = "btnNewGame"; btnNewGame.ClientRectangle = new Rectangle(12, 42, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnNewGame.IdleTexture = AssetLoader.LoadTexture("133pxbtn.png"); btnNewGame.HoverTexture = AssetLoader.LoadTexture("133pxbtn_c.png"); btnNewGame.FontIndex = 1; btnNewGame.Text = "New Game".L10N("Client:Main:NewGame"); btnNewGame.HoverSoundEffect = new EnhancedSoundEffect("button.wav"); btnNewGame.LeftClick += BtnNewGame_LeftClick; btnLoadGame = new XNAButton(WindowManager); btnLoadGame.Name = "btnLoadGame"; btnLoadGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLoadGame.IdleTexture = btnNewGame.IdleTexture; btnLoadGame.HoverTexture = btnNewGame.HoverTexture; btnLoadGame.FontIndex = 1; btnLoadGame.Text = "Load Game".L10N("Client:Main:LoadGame"); btnLoadGame.HoverSoundEffect = btnNewGame.HoverSoundEffect; btnLoadGame.LeftClick += BtnLoadGame_LeftClick; btnCancel = new XNAButton(WindowManager); btnCancel.Name = "btnCancel"; btnCancel.ClientRectangle = new Rectangle(btnLoadGame.Right + 12, btnNewGame.Y, 133, 23); btnCancel.IdleTexture = btnNewGame.IdleTexture; btnCancel.HoverTexture = btnNewGame.HoverTexture; btnCancel.FontIndex = 1; btnCancel.Text = "Cancel".L10N("Client:Main:ButtonCancel"); btnCancel.HoverSoundEffect = btnNewGame.HoverSoundEffect; btnCancel.LeftClick += BtnCancel_LeftClick; AddChild(btnNewGame); AddChild(btnLoadGame); AddChild(btnCancel); base.Initialize(); CenterOnParent(); } private void BtnNewGame_LeftClick(object sender, EventArgs e) { Disable(); NewGame?.Invoke(this, EventArgs.Empty); } private void BtnLoadGame_LeftClick(object sender, EventArgs e) { Disable(); IniFile iniFile = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); LoadGame?.Invoke(this, new GameLoadEventArgs(iniFile.GetIntValue("Settings", "GameID", -1))); } private void BtnCancel_LeftClick(object sender, EventArgs e) { Disable(); } public void Open() { btnLoadGame.AllowClick = AllowLoadingGame(); Enable(); } private bool AllowLoadingGame() { FileInfo savedGameSpawnIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI); if (!savedGameSpawnIniFile.Exists) return false; IniFile iniFile = new IniFile(savedGameSpawnIniFile.FullName); if (iniFile.GetStringValue("Settings", "Name", string.Empty) != ProgramConstants.PLAYERNAME) return false; if (!iniFile.GetBooleanValue("Settings", "Host", false)) return false; // Don't allow loading CnCNet games in LAN mode if (iniFile.SectionExists("Tunnel")) return false; return true; } } public class GameLoadEventArgs : EventArgs { public GameLoadEventArgs(int loadedGameId) { LoadedGameID = loadedGameId; } public int LoadedGameID { get; private set; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs ================================================ using ClientCore; using DTAClient.Domain; using DTAClient.Domain.LAN; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.LAN; using DTAClient.DXGUI.Multiplayer.GameLobby; using DTAClient.Online; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace DTAClient.DXGUI.Multiplayer { class LANGameLoadingLobby : GameLoadingLobbyBase { private const double DROPOUT_TIMEOUT = 20.0; private const double GAME_BROADCAST_INTERVAL = 2.0; private const string OPTIONS_COMMAND = "OPTS"; private const string GAME_LAUNCH_COMMAND = "START"; private const string READY_STATUS_COMMAND = "READY"; private const string CHAT_COMMAND = "CHAT"; private const string PLAYER_QUIT_COMMAND = "QUIT"; private const string PLAYER_JOIN_COMMAND = "JOIN"; private const string FILE_HASH_COMMAND = "FHASH"; public LANGameLoadingLobby( WindowManager windowManager, LANColor[] chatColors, MapLoader mapLoader, DiscordHandler discordHandler ) : base(windowManager, discordHandler) { encoding = ProgramConstants.LAN_ENCODING; this.chatColors = chatColors; this.mapLoader = mapLoader; localGame = ClientConfiguration.Instance.LocalGame; hostCommandHandlers = new LANServerCommandHandler[] { new ServerStringCommandHandler(CHAT_COMMAND, Server_HandleChatMessage), new ServerStringCommandHandler(FILE_HASH_COMMAND, Server_HandleFileHashMessage), new ServerNoParamCommandHandler(READY_STATUS_COMMAND, Server_HandleReadyRequest), }; playerCommandHandlers = new LANClientCommandHandler[] { new ClientStringCommandHandler(CHAT_COMMAND, Client_HandleChatMessage), new ClientStringCommandHandler(OPTIONS_COMMAND, Client_HandleOptionsMessage), new ClientNoParamCommandHandler(GAME_LAUNCH_COMMAND, Client_HandleStartCommand), new ClientNoParamCommandHandler(PLAYER_QUIT_COMMAND, HandleHostQuit), }; WindowManager.GameClosing += WindowManager_GameClosing; } private void WindowManager_GameClosing(object sender, EventArgs e) { if (client != null && client.Connected) Clear(); } public event EventHandler LobbyNotification; public event EventHandler GameBroadcast; private TcpListener listener; private TcpClient client; private IPEndPoint hostEndPoint; private LANColor[] chatColors; private readonly MapLoader mapLoader; private int chatColorIndex; private Encoding encoding; private LANServerCommandHandler[] hostCommandHandlers; private LANClientCommandHandler[] playerCommandHandlers; private TimeSpan timeSinceGameBroadcast = TimeSpan.Zero; private TimeSpan timeSinceLastReceivedCommand = TimeSpan.Zero; private string overMessage = string.Empty; private string localGame; private string localFileHash; private IReadOnlyList gameModes => mapLoader.GameModes; private int loadedGameId; private bool started = false; private volatile bool leaving; private int sessionId; public void SetUp(bool isHost, IPEndPoint hostEndPoint, TcpClient client, int loadedGameId) { leaving = false; sessionId++; Refresh(isHost); this.hostEndPoint = hostEndPoint; this.loadedGameId = loadedGameId; started = false; if (isHost) { Thread thread = new Thread(ListenForClients); thread.Start(); this.client = new TcpClient(); this.client.Connect("127.0.0.1", ProgramConstants.LAN_GAME_LOBBY_PORT); byte[] buffer = encoding.GetBytes(PLAYER_JOIN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId); this.client.GetStream().Write(buffer, 0, buffer.Length); this.client.GetStream().Flush(); var fhc = new FileHashCalculator(); fhc.CalculateHashes(); localFileHash = fhc.GetCompleteHash(); } else { this.client = client; } new Thread(HandleServerCommunication).Start(); if (IsHost) CopyPlayerDataToUI(); WindowManager.SelectedControl = tbChatInput; } public void PostJoin() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(); SendMessageToHost(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash()); UpdateDiscordPresence(true); } #region Server code private void ListenForClients() { listener = new TcpListener(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); listener.Start(); while (true) { TcpClient client; try { client = listener.AcceptTcpClient(); } catch (Exception ex) { Logger.Log("Listener error: " + ex.ToString()); break; } Logger.Log("New client connected from " + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString()); LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); lpInfo.SetClient(client); Thread thread = new Thread(new ParameterizedThreadStart(HandleClientConnection)); thread.Start(lpInfo); } } private void HandleClientConnection(object clientInfo) { var lpInfo = (LANPlayerInfo)clientInfo; byte[] message = new byte[1024]; while (true) { int bytesRead = 0; try { bytesRead = lpInfo.TcpClient.GetStream().Read(message, 0, message.Length); } catch (Exception ex) { Logger.Log("Socket error with client " + lpInfo.IPAddress + "; removing. Message: " + ex.ToString()); break; } if (bytesRead == 0) { Logger.Log("Connect attempt from " + lpInfo.IPAddress + " failed! (0 bytes read)"); break; } string msg = encoding.GetString(message, 0, bytesRead); string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length != 3) break; string name = parts[1].Trim(); int loadedGameId = Conversions.IntFromString(parts[2], -1); if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name) && loadedGameId == this.loadedGameId) { lpInfo.Name = name; AddCallback(new Action(AddPlayer), lpInfo); return; } break; } if (lpInfo.TcpClient.Connected) lpInfo.TcpClient.Close(); } private void AddPlayer(LANPlayerInfo lpInfo) { if (Players.Find(p => p.Name == lpInfo.Name) != null || Players.Count >= SGPlayers.Count || SGPlayers.Find(p => p.Name == lpInfo.Name) == null) { lpInfo.TcpClient.Close(); return; } if (Players.Count == 0) lpInfo.Ready = true; Players.Add(lpInfo); lpInfo.MessageReceived += LpInfo_MessageReceived; lpInfo.ConnectionLost += LpInfo_ConnectionLost; sndJoinSound.Play(); AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); lpInfo.StartReceiveLoop(); CopyPlayerDataToUI(); BroadcastOptions(); UpdateDiscordPresence(); } private void LpInfo_ConnectionLost(object sender, EventArgs e) { AddCallback(new Action(HandleConnectionLost), (LANPlayerInfo)sender); } private void HandleConnectionLost(LANPlayerInfo lpInfo) { CleanUpPlayer(lpInfo); Players.Remove(lpInfo); AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); sndLeaveSound.Play(); CopyPlayerDataToUI(); BroadcastOptions(); UpdateDiscordPresence(); } private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e) { AddCallback(new Action(HandleClientMessage), e.Message, (LANPlayerInfo)sender); } private void HandleClientMessage(string data, LANPlayerInfo lpInfo) { lpInfo.TimeSinceLastReceivedMessage = TimeSpan.Zero; foreach (var cmdHandler in hostCommandHandlers) { if (cmdHandler.Handle(lpInfo, data)) return; } Logger.Log("Unknown LAN command from " + lpInfo.ToString() + " : " + data); } private void CleanUpPlayer(LANPlayerInfo lpInfo) { lpInfo.MessageReceived -= LpInfo_MessageReceived; lpInfo.ConnectionLost -= LpInfo_ConnectionLost; lpInfo.TcpClient.Close(); } #endregion private void HandleServerCommunication() { byte[] message = new byte[1024]; var msg = string.Empty; int bytesRead = 0; int mySessionId = sessionId; if (!client.Connected) return; var stream = client.GetStream(); while (true) { bytesRead = 0; try { bytesRead = stream.Read(message, 0, message.Length); } catch (Exception ex) { if (leaving) break; Logger.Log("Reading data from the server failed! Message: " + ex.ToString()); AddCallback(() => { if (sessionId == mySessionId) LeaveGame(); }); break; } if (bytesRead > 0) { msg = encoding.GetString(message, 0, bytesRead); msg = overMessage + msg; List commands = new List(); while (true) { int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); if (index == -1) { overMessage = msg; break; } else { commands.Add(msg.Substring(0, index)); msg = msg.Substring(index + 1); } } foreach (string cmd in commands) { string capturedCmd = cmd; AddCallback(() => { if (sessionId == mySessionId) HandleMessageFromServer(capturedCmd); }); } continue; } if (leaving) break; Logger.Log("Reading data from the server failed (0 bytes received)!"); AddCallback(() => { if (sessionId == mySessionId) LeaveGame(); }); break; } } private void HandleMessageFromServer(string message) { timeSinceLastReceivedCommand = TimeSpan.Zero; foreach (var cmdHandler in playerCommandHandlers) { if (cmdHandler.Handle(message)) return; } Logger.Log("Unknown LAN command from the server: " + message); } private void HandleHostQuit() { if (!IsHost && !leaving) LeaveGame(); } protected override void LeaveGame() { if (leaving) return; Clear(); Disable(); base.LeaveGame(); } private void Clear() { if (IsHost) { GameBroadcast?.Invoke(this, new GameBroadcastEventArgs("GAMECLOSED")); BroadcastMessage(PLAYER_QUIT_COMMAND); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); listener.Stop(); } else { SendMessageToHost(PLAYER_QUIT_COMMAND); } leaving = true; if (this.client.Connected) this.client.Close(); } protected override void AddNotice(string message, Color color) { lbChatMessages.AddMessage(null, message, color); } protected override void BroadcastOptions() { if (Players.Count > 0) Players[0].Ready = true; var sb = new ExtendedStringBuilder(OPTIONS_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ddSavedGame.SelectedIndex); foreach (PlayerInfo pInfo in Players) { sb.Append(pInfo.Name); sb.Append(Convert.ToInt32(pInfo.Ready)); sb.Append(pInfo.IPAddress); } BroadcastMessage(sb.ToString()); } protected override void HostStartGame() { BroadcastMessage(GAME_LAUNCH_COMMAND); } protected override void RequestReadyStatus() { SendMessageToHost(READY_STATUS_COMMAND); } protected override void SendChatMessage(string message) { SendMessageToHost(CHAT_COMMAND + " " + chatColorIndex + ProgramConstants.LAN_DATA_SEPARATOR + message); sndMessageSound.Play(); } #region Server's command handlers private void Server_HandleChatMessage(LANPlayerInfo sender, string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length < 2) return; int colorIndex = Conversions.IntFromString(parts[0], -1); if (colorIndex < 0 || colorIndex >= chatColors.Length) return; BroadcastMessage(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + ProgramConstants.LAN_DATA_SEPARATOR + data); } private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) { if (hash != localFileHash) AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), sender.Name), Color.Red); sender.HashReceived = true; } private void Server_HandleReadyRequest(LANPlayerInfo sender) { if (!sender.Ready) { sender.Ready = true; CopyPlayerDataToUI(); BroadcastOptions(); } } #endregion #region Client's command handlers private void Client_HandleChatMessage(string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); if (parts.Length < 3) return; string playerName = parts[0]; int colorIndex = Conversions.IntFromString(parts[1], -1); if (colorIndex < 0 || colorIndex >= chatColors.Length) return; lbChatMessages.AddMessage(new ChatMessage(playerName, chatColors[colorIndex].XNAColor, DateTime.Now, parts[2])); sndMessageSound.Play(); } private void Client_HandleOptionsMessage(string data) { if (IsHost) return; string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); const int PLAYER_INFO_PARTS = 3; int pCount = (parts.Length - 1) / PLAYER_INFO_PARTS; if (pCount * PLAYER_INFO_PARTS + 1 != parts.Length) return; int savedGameIndex = Conversions.IntFromString(parts[0], -1); if (savedGameIndex < 0 || savedGameIndex >= ddSavedGame.Items.Count) { return; } ddSavedGame.SelectedIndex = savedGameIndex; Players.Clear(); for (int i = 0; i < pCount; i++) { int baseIndex = 1 + i * PLAYER_INFO_PARTS; string pName = parts[baseIndex]; bool ready = Conversions.IntFromString(parts[baseIndex + 1], -1) > 0; string ipAddress = parts[baseIndex + 2]; LANPlayerInfo pInfo = new LANPlayerInfo(encoding); pInfo.Name = pName; pInfo.Ready = ready; pInfo.IPAddress = ipAddress; Players.Add(pInfo); } if (Players.Count > 0) // Set IP of host Players[0].IPAddress = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); CopyPlayerDataToUI(); } private void Client_HandleStartCommand() { started = true; LoadGame(); } #endregion /// /// Broadcasts a command to all players in the game as the game host. /// /// The command to send. private void BroadcastMessage(string message) { if (!IsHost) return; foreach (PlayerInfo pInfo in Players) { var lpInfo = (LANPlayerInfo)pInfo; lpInfo.SendMessage(message); } } private void SendMessageToHost(string message) { if (!client.Connected) return; byte[] buffer = encoding.GetBytes( message + ProgramConstants.LAN_MESSAGE_SEPARATOR); NetworkStream ns = client.GetStream(); try { ns.Write(buffer, 0, buffer.Length); ns.Flush(); } catch { Logger.Log("Sending message to game host failed!"); } } public void SetChatColorIndex(int colorIndex) { chatColorIndex = colorIndex; } public override string GetSwitchName() { return "Load Game".L10N("Client:Main:LoadGameSwitchName"); } public override void Update(GameTime gameTime) { if (IsHost) { for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; if (!lpInfo.Update(gameTime)) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); BroadcastOptions(); UpdateDiscordPresence(); i--; } } timeSinceGameBroadcast += gameTime.ElapsedGameTime; if (timeSinceGameBroadcast > TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL)) { BroadcastGame(); timeSinceGameBroadcast = TimeSpan.Zero; } } else { timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) { LobbyNotification?.Invoke(this, new LobbyNotificationEventArgs("Connection to the game host timed out.".L10N("Client:Main:HostConnectTimeOut"))); LeaveGame(); } } base.Update(gameTime); } private void BroadcastGame() { var sb = new ExtendedStringBuilder("GAME ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION); sb.Append(ProgramConstants.GAME_VERSION); sb.Append(localGame); sb.Append((string)lblMapNameValue.Tag); sb.Append((string)lblGameModeValue.Tag); sb.Append(0); // LoadedGameID var sbPlayers = new StringBuilder(); SGPlayers.ForEach(p => sbPlayers.Append(p.Name + ",")); sbPlayers.Remove(sbPlayers.Length - 1, 1); sb.Append(sbPlayers.ToString()); sb.Append(Convert.ToInt32(started || Players.Count == SGPlayers.Count)); sb.Append(1); // IsLoadedGame sb.Append(string.Empty); // MapHash GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString())); } protected override void HandleGameProcessExited() { base.HandleGameProcessExited(); LeaveGame(); } protected override void UpdateDiscordPresence(bool resetTimer = false) { if (discordHandler == null) return; PlayerInfo player = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); if (player == null) return; string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings discordHandler.UpdatePresence( (string)lblMapNameValue.Tag, (string)lblGameModeValue.Tag, currentState, "LAN", Players.Count, SGPlayers.Count, "LAN Game", IsHost, resetTimer); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANLobby.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text; using ClientCore; using ClientCore.Extensions; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.LAN; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Domain.Multiplayer.LAN; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby; using DTAClient.Online; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer { class LANLobby : XNAWindow { private const double ALIVE_MESSAGE_INTERVAL = 5.0; private const double INACTIVITY_REMOVE_TIME = 10.0; private const double GAME_INACTIVITY_REMOVE_TIME = 20.0; private const double MESSAGE_ID_EXPIRATION_SECONDS = 60.0; public LANLobby( WindowManager windowManager, GameCollection gameCollection, MapLoader mapLoader, DiscordHandler discordHandler, Random random ) : base(windowManager) { this.gameCollection = gameCollection; this.mapLoader = mapLoader; this.discordHandler = discordHandler; this.random = random; } public event EventHandler Exited; private Random random; XNAClientButton btnMainMenu; XNAClientButton btnNewGame; XNAClientButton btnJoinGame; XNAChatTextBox tbChatInput; XNALabel lblColor; XNAClientDropDown ddColor; LANGameCreationWindow gameCreationWindow; LANGameLobby lanGameLobby; LANGameLoadingLobby lanGameLoadingLobby; Texture2D unknownGameIcon; LANColor[] chatColors; string localGame; int localGameIndex; GameCollection gameCollection; private IReadOnlyList gameModes => mapLoader.GameModes; TimeSpan timeSinceGameRefresh = TimeSpan.Zero; EnhancedSoundEffect sndGameCreated; Encoding encoding; ChatListBox lbChatMessages; GameListBox lbGameList; // lbPlayerList is now managed by LANPlayerManager `playerManager` // XNAListBox lbPlayerList; LANPlayerManager playerManager; LANMessageDeduplicator messageDeduplicator; LANLobbyBroadcastManager broadcastManager; TimeSpan timeSinceAliveMessage = TimeSpan.Zero; MapLoader mapLoader; DiscordHandler discordHandler; PrivateMessagingWindow pmWindow; public override void Initialize() { Name = "LANLobby"; BackgroundTexture = AssetLoader.LoadTexture("cncnetlobbybg.png"); ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 64, WindowManager.RenderResolutionY - 64); localGame = ClientConfiguration.Instance.LocalGame; localGameIndex = gameCollection.GameList.FindIndex( g => g.InternalName.ToUpper() == localGame.ToUpper()); btnNewGame = new XNAClientButton(WindowManager); btnNewGame.Name = "btnNewGame"; btnNewGame.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnNewGame.Text = "Create Game".L10N("Client:Main:CreateGame"); btnNewGame.LeftClick += BtnNewGame_LeftClick; btnJoinGame = new XNAClientButton(WindowManager); btnJoinGame.Name = "btnJoinGame"; btnJoinGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnJoinGame.Text = "Join Game".L10N("Client:Main:JoinGame"); btnJoinGame.LeftClick += BtnJoinGame_LeftClick; btnMainMenu = new XNAClientButton(WindowManager); btnMainMenu.Name = "btnMainMenu"; btnMainMenu.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnMainMenu.Text = "Main Menu".L10N("Client:Main:MainMenu"); btnMainMenu.LeftClick += BtnMainMenu_LeftClick; lbGameList = new GameListBox(WindowManager, mapLoader, localGame); lbGameList.Name = "lbGameList"; lbGameList.ClientRectangle = new Rectangle(btnNewGame.X, 41, btnJoinGame.Right - btnNewGame.X, btnNewGame.Y - 53); lbGameList.GameLifetime = 15.0; // Smaller lifetime in LAN lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbGameList.DoubleLeftClick += LbGameList_DoubleLeftClick; lbGameList.AllowMultiLineItems = false; var lbPlayerList = new XNAListBox(WindowManager); lbPlayerList.Name = "lbPlayerList"; lbPlayerList.ClientRectangle = new Rectangle(Width - 202, lbGameList.Y, 190, lbGameList.Height); lbPlayerList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbPlayerList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbPlayerList.LineHeight = 16; lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = "lbChatMessages"; lbChatMessages.ClientRectangle = new Rectangle(lbGameList.Right + 12, lbGameList.Y, lbPlayerList.X - lbGameList.Right - 24, lbGameList.Height); lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbChatMessages.LineHeight = 16; tbChatInput = new XNAChatTextBox(WindowManager); tbChatInput.Name = "tbChatInput"; tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X, btnNewGame.Y, lbChatMessages.Width, btnNewGame.Height); tbChatInput.Suggestion = "Type here to chat...".L10N("Client:Main:ChatHere"); tbChatInput.MaximumTextLength = 200; tbChatInput.EnterPressed += TbChatInput_EnterPressed; lblColor = new XNALabel(WindowManager); lblColor.Name = "lblColor"; lblColor.ClientRectangle = new Rectangle(lbChatMessages.X, 14, 0, 0); lblColor.FontIndex = 1; lblColor.Text = "YOUR COLOR:".L10N("Client:Main:YourColor"); ddColor = new XNAClientDropDown(WindowManager); ddColor.Name = "ddColor"; ddColor.ClientRectangle = new Rectangle(lblColor.X + 95, 12, 150, 21); chatColors = new LANColor[] { new LANColor("Gray".L10N("Client:Main:ColorGray"), Color.Gray), new LANColor("Metallic".L10N("Client:Main:ColorLightGrayMetallic"), Color.LightGray), new LANColor("Green".L10N("Client:Main:ColorGreen"), Color.ForestGreen), new LANColor("Lime Green".L10N("Client:Main:ColorLimeGreen"), Color.LimeGreen), new LANColor("Green Yellow".L10N("Client:Main:ColorGreenYellow"), Color.GreenYellow), new LANColor("Goldenrod".L10N("Client:Main:ColorGoldenrod"), Color.Goldenrod), new LANColor("Yellow".L10N("Client:Main:ColorYellow"), Color.Yellow), new LANColor("Orange".L10N("Client:Main:ColorOrange"), Color.Orange), new LANColor("Red".L10N("Client:Main:ColorRed"), Color.Red), new LANColor("Pink".L10N("Client:Main:ColorPink"), Color.DeepPink), new LANColor("Purple".L10N("Client:Main:ColorPurple"), Color.MediumPurple), new LANColor("Sky Blue".L10N("Client:Main:ColorSkyBlue"), Color.LightSkyBlue), new LANColor("Blue".L10N("Client:Main:ColorBlue"), Color.RoyalBlue), new LANColor("Brown".L10N("Client:Main:ColorBrown"), Color.SaddleBrown), new LANColor("Teal".L10N("Client:Main:ColorTeal"), Color.Teal) }; foreach (LANColor color in chatColors) { ddColor.AddItem(color.Name, color.XNAColor); } AddChild(btnNewGame); AddChild(btnJoinGame); AddChild(btnMainMenu); AddChild(lbPlayerList); AddChild(lbChatMessages); AddChild(lbGameList); AddChild(tbChatInput); AddChild(lblColor); AddChild(ddColor); gameCreationWindow = new LANGameCreationWindow(WindowManager); var gameCreationPanel = new DarkeningPanel(WindowManager); AddChild(gameCreationPanel); gameCreationPanel.AddChild(gameCreationWindow); gameCreationWindow.Disable(); gameCreationWindow.NewGame += GameCreationWindow_NewGame; gameCreationWindow.LoadGame += GameCreationWindow_LoadGame; // Initialize player manager after lbPlayerList is created playerManager = new LANPlayerManager(lbPlayerList); // Initialize message deduplicator with a random seed messageDeduplicator = new LANMessageDeduplicator(random.Next(), MESSAGE_ID_EXPIRATION_SECONDS); // Initialize broadcast manager encoding = Encoding.UTF8; broadcastManager = new LANLobbyBroadcastManager(ProgramConstants.LAN_LOBBY_PORT, encoding); // Dispatch to UI thread broadcastManager.MessageReceived += (sender, e) => AddCallback(() => HandleNetworkMessage(e.Data, e.EndPoint)); var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream unknownIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.unknownicon.png"); unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); sndGameCreated = new EnhancedSoundEffect("gamecreated.wav"); base.Initialize(); CenterOnParent(); gameCreationPanel.SetPositionAndSize(); lanGameLobby = new LANGameLobby(WindowManager, "MultiplayerGameLobby", null, chatColors, mapLoader, discordHandler, pmWindow, random); DarkeningPanel.AddAndInitializeWithControl(WindowManager, lanGameLobby); lanGameLobby.Disable(); lanGameLoadingLobby = new LANGameLoadingLobby(WindowManager, chatColors, mapLoader, discordHandler); DarkeningPanel.AddAndInitializeWithControl(WindowManager, lanGameLoadingLobby); lanGameLoadingLobby.Disable(); int selectedColor = UserINISettings.Instance.LANChatColor; ddColor.SelectedIndex = selectedColor >= ddColor.Items.Count || selectedColor < 0 ? 0 : selectedColor; SetChatColor(); ddColor.SelectedIndexChanged += DdColor_SelectedIndexChanged; lanGameLobby.GameLeft += LanGameLobby_GameLeft; lanGameLobby.GameBroadcast += LanGameLobby_GameBroadcast; lanGameLoadingLobby.GameBroadcast += LanGameLoadingLobby_GameBroadcast; lanGameLoadingLobby.GameLeft += LanGameLoadingLobby_GameLeft; WindowManager.GameClosing += WindowManager_GameClosing; } private void LanGameLoadingLobby_GameLeft(object sender, EventArgs e) { Enable(); } private void WindowManager_GameClosing(object sender, EventArgs e) { SendMessage("QUIT"); // Dispose the broadcast manager (which closes the socket and stops the listener) broadcastManager?.Dispose(); // Dispose the message deduplicator to stop the cleanup timer messageDeduplicator?.Dispose(); } private void LanGameLobby_GameBroadcast(object sender, GameBroadcastEventArgs e) { SendMessage(e.Message); } private void LanGameLobby_GameLeft(object sender, GameLeftEventArgs e) { if (!string.IsNullOrWhiteSpace(e.Message)) AddChatMessage(new ChatMessage(Color.Red, e.Message)); Enable(); } private void LanGameLoadingLobby_GameBroadcast(object sender, GameBroadcastEventArgs e) { SendMessage(e.Message); } private void GameCreationWindow_LoadGame(object sender, GameLoadEventArgs e) { lanGameLoadingLobby.SetUp(true, new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null, e.LoadedGameID); lanGameLoadingLobby.Enable(); } private void GameCreationWindow_NewGame(object sender, EventArgs e) { lanGameLobby.SetUp(true, new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); lanGameLobby.Enable(); } private void SetChatColor() { tbChatInput.TextColor = chatColors[ddColor.SelectedIndex].XNAColor; lanGameLobby.SetChatColorIndex(ddColor.SelectedIndex); UserINISettings.Instance.LANChatColor.Value = ddColor.SelectedIndex; } private void DdColor_SelectedIndexChanged(object sender, EventArgs e) { SetChatColor(); UserINISettings.Instance.SaveSettings(); } void AddChatMessage(ChatMessage message) => lbChatMessages.AddMessage(message); void AddChatMessage(string message) => lbChatMessages.AddMessage(message); void AddChatMessage(string sender, string message, Color color) => lbChatMessages.AddMessage(sender, message, color); public void Open() { playerManager.Clear(); messageDeduplicator.Clear(); lbGameList.ClearGames(); Visible = true; Enabled = true; try { broadcastManager.Initialize(); } catch (Exception ex) { AddChatMessage(new ChatMessage(Color.Red, "Creating LAN socket failed! Message:".L10N("Client:Main:SocketFailure1") + " " + ex.Message + "\n" + "Please check your firewall settings.".L10N("Client:Main:SocketFailure2") + " " + "Also make sure that no other application is listening to traffic on UDP ports 1232 - 1234.".L10N("Client:Main:SocketFailure3"))); return; } SendAlive(); } private void SendMessage(string message) { // Wrap message with message ID at the beginning string wrappedMessage = messageDeduplicator.WrapMessage(message); bool sendSucceeded = broadcastManager.SendMessage(wrappedMessage); if (!sendSucceeded) { // Socket is not initialized or sending failed; report this so failures are not silent. AddChatMessage(new ChatMessage(Color.Red, "Failed to send LAN broadcast message. The network socket may not be initialized.")); } } private void HandleNetworkMessage(string data, IPEndPoint endPoint) { // Unwrap message to extract message ID and check for duplicates messageDeduplicator.UnwrapMessage(data, out string payload, out bool isDuplicate); if (isDuplicate || string.IsNullOrWhiteSpace(payload)) return; string[] commandAndParams = payload.Split(' '); string command = commandAndParams[0]; string[] parameters; { // For parameterless commands like "QUIT", avoid the potential out-of-bounds issue by locating the first space first. int firstSpace = payload.IndexOf(' '); parameters = firstSpace >= 0 ? payload.Substring(firstSpace + 1).Split([ProgramConstants.LAN_DATA_SEPARATOR]) : []; } LANLobbyUser user = playerManager.GetPlayerIfExist(endPoint); switch (command) { case "ALIVE": if (parameters.Length < 2) return; int gameIndex = Conversions.IntFromString(parameters[0], -1); string name = parameters[1]; if (user == null) { Texture2D gameTexture = unknownGameIcon; if (gameIndex > -1 && gameIndex < gameCollection.GameList.Count) gameTexture = gameCollection.GameList[gameIndex].Texture; user = playerManager.GetOrCreatePlayer(endPoint, name, gameTexture); } user.ClearTimeWithoutRefresh(); break; case "CHAT": if (user == null) return; if (parameters.Length < 2) return; int colorIndex = Conversions.IntFromString(parameters[0], -1); if (colorIndex < 0 || colorIndex >= chatColors.Length) return; AddChatMessage(new ChatMessage(user.Name, chatColors[colorIndex].XNAColor, DateTime.Now, parameters[1])); break; case "QUIT": if (user == null) return; playerManager.RemovePlayer(endPoint); break; case "GAMECLOSED": int closedGameIndex = lbGameList.HostedGames.FindIndex(g => ((HostedLANGame)g).EndPoint.Equals(endPoint)); if (closedGameIndex > -1) { lbGameList.HostedGames.RemoveAt(closedGameIndex); lbGameList.Refresh(); } break; case "GAME": if (user == null) return; HostedLANGame game = new HostedLANGame(); if (!game.SetDataFromStringArray(gameCollection, parameters)) return; game.EndPoint = endPoint; int existingGameIndex = lbGameList.HostedGames.FindIndex(g => ((HostedLANGame)g).EndPoint.Equals(endPoint)); if (existingGameIndex > -1) lbGameList.HostedGames[existingGameIndex] = game; else lbGameList.HostedGames.Add(game); lbGameList.Refresh(); break; } } private void SendAlive() { StringBuilder sb = new StringBuilder("ALIVE "); sb.Append(localGameIndex); sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(ProgramConstants.PLAYERNAME); SendMessage(sb.ToString()); timeSinceAliveMessage = TimeSpan.Zero; } private void TbChatInput_EnterPressed(object sender, EventArgs e) { if (string.IsNullOrEmpty(tbChatInput.Text)) return; string chatMessage = tbChatInput.Text.Replace((char)01, '?'); StringBuilder sb = new StringBuilder("CHAT "); sb.Append(ddColor.SelectedIndex); sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(chatMessage); SendMessage(sb.ToString()); tbChatInput.Text = string.Empty; } private void LbGameList_DoubleLeftClick(object sender, EventArgs e) { if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count) return; HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag; if (hg.Game.InternalName.ToUpper() != localGame.ToUpper()) { AddChatMessage( string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); return; } if (hg.Locked) { AddChatMessage(string.Format("The game {0} is locked!".L10N("Client:Main:GameLockedWithName"), hg.RoomName)); return; } if (hg.IsLoadedGame) { if (!hg.Players.Contains(ProgramConstants.PLAYERNAME)) { AddChatMessage("You do not exist in the saved game!".L10N("Client:Main:NotInSavedGame")); return; } } else { if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) { AddChatMessage("Your name is already taken in the game.".L10N("Client:Main:NameOccupied")); return; } } if (hg.GameVersion != ProgramConstants.GAME_VERSION) { AddChatMessage(new ChatMessage(Color.Yellow, "The game host is on a different game version than you. Version incompatibilities may cause issues.".L10N("Client:Main:JoinGameVersionMismatch"))); } AddChatMessage(string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); try { var client = new TcpClient(hg.EndPoint.Address.ToString(), ProgramConstants.LAN_GAME_LOBBY_PORT); byte[] buffer; if (hg.IsLoadedGame) { var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); int loadedGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); lanGameLoadingLobby.SetUp(false, hg.EndPoint, client, loadedGameId); lanGameLoadingLobby.Enable(); buffer = encoding.GetBytes("JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR); client.GetStream().Write(buffer, 0, buffer.Length); client.GetStream().Flush(); lanGameLoadingLobby.PostJoin(); } else { lanGameLobby.SetUp(false, hg.EndPoint, client); lanGameLobby.Enable(); buffer = encoding.GetBytes("JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR); client.GetStream().Write(buffer, 0, buffer.Length); client.GetStream().Flush(); lanGameLobby.PostJoin(); } } catch (Exception ex) { AddChatMessage(null, "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); } } private void BtnMainMenu_LeftClick(object sender, EventArgs e) { Visible = false; Enabled = false; SendMessage("QUIT"); broadcastManager.Shutdown(); Exited?.Invoke(this, EventArgs.Empty); } private void BtnJoinGame_LeftClick(object sender, EventArgs e) { LbGameList_DoubleLeftClick(this, EventArgs.Empty); } private void BtnNewGame_LeftClick(object sender, EventArgs e) { if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) gameCreationWindow.Open(); else GameCreationWindow_NewGame(sender, e); } public override void Update(GameTime gameTime) { // Remove inactive players periodically { // Get a thread-safe snapshot of all players var playersCopy = playerManager.GetAllPlayers(); foreach (var player in playersCopy) { player.AddToTimeWithoutRefresh(gameTime.ElapsedGameTime); if (player.TimeWithoutRefresh > TimeSpan.FromSeconds(INACTIVITY_REMOVE_TIME)) playerManager.RemovePlayer(player.EndPoint); } } // Send ALIVE message periodically timeSinceAliveMessage += gameTime.ElapsedGameTime; if (timeSinceAliveMessage > TimeSpan.FromSeconds(ALIVE_MESSAGE_INTERVAL)) SendAlive(); base.Update(gameTime); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANLobbyBroadcastManager.cs ================================================ #nullable enable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; using Rampastring.Tools; using NetworkInterface = System.Net.NetworkInformation.NetworkInterface; namespace DTAClient.DXGUI.Multiplayer; /// /// Thread-safe manager for LAN lobby broadcasting and network communication. /// Encapsulates socket management, broadcast interface discovery, message sending, /// and network listening to ensure thread-safe operations. /// /// This class broadcasts messages to all available network interfaces, and provides a message ID based de-duplication mechanism. /// By broadcasting to all interfaces, it ensures that messages reach all clients. /// Otherwise, Sending UDP to 255.255.255.255 typically uses the network card with the lowest metric -- this does not fit the use case of players using a dedicated interface for gaming, such as VPNs or a secondary router without Internet access. /// internal class LANLobbyBroadcastManager : IDisposable { private readonly object socketLock = new(); private readonly ConcurrentDictionary broadcastInterfaces = new(); private readonly Encoding encoding; private readonly int lobbyPort; private Socket? socket; private Thread? listener; private Thread? interfaceRefresher; private volatile bool stopRefresher = false; private int disposed = 0; /// /// Event raised when a network message is received. /// The event is raised on the listener thread; subscribers are responsible /// for marshaling to the main/UI thread if required. /// public event EventHandler? MessageReceived; /// /// Record for storing network interface information. /// /// The local IP address of this interface. /// The broadcast endpoint for this interface. private record PlayerNetworkInterface(IPAddress LocalIP, IPEndPoint Broadcast); /// /// Initializes a new instance of the LANLobbyBroadcastManager class. /// /// The UDP port to bind for LAN lobby communication. /// The text encoding to use for messages (typically UTF-8). public LANLobbyBroadcastManager(int lobbyPort, Encoding encoding) { this.lobbyPort = lobbyPort; this.encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); } /// /// Gets whether the socket is successfully initialized and bound. /// public bool IsInitialized { get { lock (socketLock) { return socket != null && socket.IsBound; } } } /// /// Initializes the socket, binds it to the lobby port, and starts listening for messages. /// public void Initialize() { lock (socketLock) { // Clean up any existing socket if (socket != null) { try { socket.Close(); } catch (ObjectDisposedException) { // Already disposed } socket = null; } // Clear broadcast interfaces broadcastInterfaces.Clear(); Logger.Log("Creating LAN socket."); try { socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) { EnableBroadcast = true }; socket.Bind(new IPEndPoint(IPAddress.Any, lobbyPort)); // Discover initial broadcast interfaces var initialInterfaces = DiscoverBroadcastInterfaces(lobbyPort); foreach (var (key, netIf) in initialInterfaces) { broadcastInterfaces[key] = netIf; } } catch (SocketException ex) { Logger.Log("Creating LAN socket failed! Message: " + ex.ToString()); throw; } // Reset stop flag for the refresher thread stopRefresher = false; Logger.Log("Starting LAN broadcast message listener."); listener = new Thread(new ThreadStart(Listen)) { IsBackground = true }; listener.Start(); Logger.Log("Starting network interface refresh thread."); interfaceRefresher = new Thread(new ThreadStart(RefreshInterfacesPeriodically)) { IsBackground = true }; interfaceRefresher.Start(); } } /// /// Discovers all available network interfaces for broadcasting. /// This method scans only "up" network interfaces and identifies those with valid IPv4 addresses. /// /// The port to use for broadcast endpoints. /// A dictionary of network interfaces keyed by their local IP address. private static Dictionary DiscoverBroadcastInterfaces(int port) { Logger.Log("Discovering broadcast interfaces."); var discoveredInterfaces = new Dictionary(); NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterface iface in interfaces) { // Only consider interfaces that are operational (up) if (iface.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties prop = iface.GetIPProperties(); UnicastIPAddressInformation? info = prop.UnicastAddresses.FirstOrDefault(info => info.Address.AddressFamily == AddressFamily.InterNetwork); if (info == null || info.IPv4Mask == null) continue; IPAddress localIPAddress = info.Address; byte[] ipBytes = localIPAddress.GetAddressBytes(); byte[] maskBytes = info.IPv4Mask.GetAddressBytes(); byte[] broadcastBytes = new byte[ipBytes.Length]; for (int i = 0; i < ipBytes.Length; i++) { broadcastBytes[i] = (byte)(ipBytes[i] | ~maskBytes[i]); } IPAddress broadcastIP = new IPAddress(broadcastBytes); string key = localIPAddress.ToString(); var netIf = new PlayerNetworkInterface(localIPAddress, new IPEndPoint(broadcastIP, port)); discoveredInterfaces[key] = netIf; } if (discoveredInterfaces.Count == 0) { Logger.Log("Warning: No broadcast interfaces found! LAN lobby broadcasting will not function. " + "Please ensure that your network adapters are enabled and have valid IPv4 addresses."); } return discoveredInterfaces; } /// /// Sends a message to all broadcast interfaces. /// Failed interfaces are logged but not removed from the broadcast list. /// /// The message to broadcast. /// True if the message was sent successfully to at least one interface, false if the socket is not initialized or all interfaces fail. public bool SendMessage(string message) { lock (socketLock) { if (socket == null || !socket.IsBound) return false; byte[] buffer = encoding.GetBytes(message); if (broadcastInterfaces.IsEmpty) { Logger.Log("Warning: No broadcast interfaces available in SendMessage!"); } bool success = false; foreach ((string key, PlayerNetworkInterface networkInterface) in broadcastInterfaces) { try { _ = socket.SendTo(buffer, networkInterface.Broadcast); success = true; } catch (SocketException) { // Do nothing } } return success; } } /// /// Background thread that listens for incoming UDP messages. /// private void Listen() { try { while (true) { Socket? currentSocket; lock (socketLock) { currentSocket = socket; } if (currentSocket == null) break; EndPoint endPoint = new IPEndPoint(IPAddress.Any, lobbyPort); byte[] buffer = new byte[4096]; int receivedBytes = currentSocket.ReceiveFrom(buffer, ref endPoint); IPEndPoint ipEndPoint = (IPEndPoint)endPoint; string data = encoding.GetString(buffer, 0, receivedBytes); if (string.IsNullOrEmpty(data)) continue; HandleNetworkMessage(data, ipEndPoint); } } catch (Exception ex) { if (ex is SocketException socketEx && socketEx.SocketErrorCode == SocketError.Interrupted) { // Do nothing; this is the expected way for the listener thread to end. } else { Logger.Log("LAN socket listener: exception: " + ex.ToString()); } } } /// /// Handles a received network message by raising the MessageReceived event. /// private void HandleNetworkMessage(string data, IPEndPoint endPoint) { MessageReceived?.Invoke(this, new LANLobbyBroadcastMessageReceivedEventArgs(data, endPoint)); } /// /// Interval in milliseconds for refreshing network interfaces. /// private const int INTERFACE_REFRESH_INTERVAL_MS = 5000; /// /// Interval in milliseconds for checking the stop signal during sleep. /// private const int STOP_CHECK_INTERVAL_MS = 100; /// /// Background thread that periodically refreshes network interfaces. /// This ensures that the broadcast list stays up-to-date with network changes. /// private void RefreshInterfacesPeriodically() { try { while (!stopRefresher) { // Sleep for the refresh interval, but check periodically for stop signal int iterations = INTERFACE_REFRESH_INTERVAL_MS / STOP_CHECK_INTERVAL_MS; for (int i = 0; i < iterations && !stopRefresher; i++) Thread.Sleep(STOP_CHECK_INTERVAL_MS); if (stopRefresher) break; // Check if we're disposed if (Volatile.Read(ref disposed) != 0) break; lock (socketLock) { // Check stop flag again inside lock to avoid race condition if (stopRefresher) break; // Check if socket is still valid if (socket == null || !socket.IsBound) break; } // Discover new interfaces outside the lock to minimize lock time var newInterfaces = DiscoverBroadcastInterfaces(lobbyPort); lock (socketLock) { // Check again after discovery in case state changed if (stopRefresher || socket == null || !socket.IsBound) break; broadcastInterfaces.Clear(); foreach (var (key, netIf) in newInterfaces) { broadcastInterfaces[key] = netIf; } } } } catch (ThreadInterruptedException) { // Expected when shutting down } catch (Exception ex) { Logger.Log("Network interface refresh thread: exception: " + ex.ToString()); } } /// /// Timeout in milliseconds for waiting for threads to terminate during shutdown. /// private const int THREAD_SHUTDOWN_TIMEOUT_MS = 1000; /// /// Closes the socket and stops the listening thread. /// public void Shutdown() { lock (socketLock) { // Signal the refresher thread to stop (inside lock for thread safety) stopRefresher = true; if (socket != null && socket.IsBound) { try { socket.Close(); } catch (ObjectDisposedException) { // Already disposed } } socket = null; } if (listener != null) { bool listenerTerminated = listener.Join(millisecondsTimeout: THREAD_SHUTDOWN_TIMEOUT_MS); if (!listenerTerminated) Logger.Log("Failed to shut down listener after timeout!"); listener = null; } if (interfaceRefresher != null) { // Interrupt the thread to wake it from sleep interfaceRefresher.Interrupt(); bool refresherTerminated = interfaceRefresher.Join(millisecondsTimeout: THREAD_SHUTDOWN_TIMEOUT_MS); if (!refresherTerminated) Logger.Log("Failed to shut down interface refresher after timeout!"); interfaceRefresher = null; } // Clear broadcast interfaces broadcastInterfaces.Clear(); } /// /// Gets the count of active broadcast interfaces. /// public int BroadcastInterfaceCount => broadcastInterfaces.Count; /// /// Disposes the broadcast manager and releases all resources. /// public void Dispose() { if (Interlocked.CompareExchange(ref disposed, 1, 0) == 0) Shutdown(); GC.SuppressFinalize(this); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANLobbyBroadcastMessageReceivedEventArgs.cs ================================================ #nullable enable using System; using System.Net; namespace DTAClient.DXGUI.Multiplayer; /// /// Event arguments for network message received events. /// internal class LANLobbyBroadcastMessageReceivedEventArgs : EventArgs { /// /// The received message data. /// public string Data { get; } /// /// The endpoint from which the message was received. /// public IPEndPoint EndPoint { get; } public LANLobbyBroadcastMessageReceivedEventArgs(string data, IPEndPoint endPoint) { Data = data; EndPoint = endPoint; } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANMessageDeduplicator.cs ================================================ #nullable enable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; namespace DTAClient.DXGUI.Multiplayer; /// /// Thread-safe message de-duplicator for LAN lobby messages. /// Generates unique message IDs for outgoing messages and tracks received message IDs /// to filter out duplicates. Message IDs expire after a configurable timeout to prevent /// memory leaks. Cleanup is performed automatically in a background thread. /// internal class LANMessageDeduplicator : IDisposable { private readonly Random random; private readonly object lockObject = new(); // Track received message IDs with their expiration time private readonly ConcurrentDictionary receivedMessageIds = new(); // Message ID expiration time in seconds private readonly double messageIdExpirationSeconds; // Background cleanup private readonly Timer cleanupTimer; private const double CLEANUP_INTERVAL_SECONDS = 30.0; private int disposed = 0; /// /// Initializes a new instance of the LANMessageDeduplicator class. /// /// Seed for the random number generator used to create message IDs. /// How long to keep message IDs before expiring them (default 60 seconds). public LANMessageDeduplicator(int randomSeed, double messageIdExpirationSeconds = 60.0) { random = new Random(randomSeed); this.messageIdExpirationSeconds = messageIdExpirationSeconds; // Start automatic cleanup timer int cleanupIntervalMs = (int)(CLEANUP_INTERVAL_SECONDS * 1000); cleanupTimer = new Timer(CleanupCallback, null, cleanupIntervalMs, cleanupIntervalMs); } private void CleanupCallback(object? state) { if (disposed == 0) CleanupExpiredMessageIds(); } /// /// Generates a unique random message ID. /// Message IDs are prefixed with "MID_" followed by 8 alphanumeric characters /// to avoid collision with legitimate message parameters. /// /// A unique message ID string. public string GenerateMessageId() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; char[] id = new char[8]; // Lock is required because Random is not thread-safe lock (lockObject) { for (int i = 0; i < id.Length; i++) id[i] = chars[random.Next(chars.Length)]; } string messageId = "MID_" + new string(id); Debug.Assert(IsValidMessageId(messageId), "Invalid message ID generated."); return messageId; } public const int MESSAGE_ID_PREFIX_LENGTH = 4; // "MID_" public const int MESSAGE_ID_LENGTH = 12; // "MID_" + 8 characters /// /// Checks if a string is a valid message ID. /// Message IDs must start with "MID_" followed by 8 alphanumeric characters. /// /// The string to validate. /// True if the string is a valid message ID, false otherwise. public static bool IsValidMessageId(string value) { return !string.IsNullOrEmpty(value) && value.StartsWith("MID_") && value.Length == MESSAGE_ID_LENGTH && value[MESSAGE_ID_PREFIX_LENGTH..].All(char.IsLetterOrDigit); } /// /// Records a received message ID and determines if it's a duplicate. /// Note: Uses DateTime.UtcNow for expiration timing. While a monotonic time source /// would be more robust against system clock adjustments, DateTime is sufficient /// for LAN lobby traffic where the 60-second expiration window is large. /// /// The message ID to record. /// True if this message ID was already recorded (duplicate), false if it's new. public void AddMessage(string messageId, out bool isDuplicate) { if (string.IsNullOrEmpty(messageId)) { // If no message ID provided, consider it not a duplicate // This maintains backward compatibility with old clients isDuplicate = false; return; } DateTime expirationTime = DateTime.UtcNow.AddSeconds(messageIdExpirationSeconds); // Try to add the message ID with expiration time in one atomic operation // If it already exists, it's a duplicate isDuplicate = !receivedMessageIds.TryAdd(messageId, expirationTime); } /// /// Wraps a message payload with a message ID at the beginning. /// /// The original message payload. /// The wrapped message with message ID prepended. public string WrapMessage(string payload) { string messageId = GenerateMessageId(); return messageId + payload; } /// /// Unwraps a message, extracting the message ID from the beginning and returning the payload. /// Also checks if the message is a duplicate. /// /// The wrapped message with message ID at the beginning. /// The unwrapped message payload. /// True if this message ID was already recorded (duplicate), false if it's new. public void UnwrapMessage(string wrappedMessage, out string payload, out bool isDuplicate) { // Check if the message starts with a valid message ID if (!string.IsNullOrEmpty(wrappedMessage) && wrappedMessage.Length >= MESSAGE_ID_LENGTH) { string potentialMessageId = wrappedMessage[..MESSAGE_ID_LENGTH]; if (IsValidMessageId(potentialMessageId)) { // Extract message ID and payload string messageId = potentialMessageId; payload = wrappedMessage[MESSAGE_ID_LENGTH..]; // Check for duplicate AddMessage(messageId, out isDuplicate); return; } } // No valid message ID found - treat as non-duplicate for backward compatibility payload = wrappedMessage; isDuplicate = false; } /// /// Removes expired message IDs from the tracking dictionary. /// This is called automatically by the background cleanup timer. /// Note: This performs O(n) enumeration of all tracked IDs. For typical LAN lobby /// traffic this is acceptable, but for high-traffic scenarios a more efficient /// data structure (e.g., priority queue) could be considered. /// ConcurrentDictionary operations (TryRemove, enumeration) are thread-safe. /// private void CleanupExpiredMessageIds() { // Check if disposed if (disposed != 0) return; // Quick exit if there's nothing to clean up if (receivedMessageIds.IsEmpty) return; DateTime now = DateTime.UtcNow; // Find all expired message IDs // ConcurrentDictionary enumeration is thread-safe List expiredIds = receivedMessageIds .Where(kvp => kvp.Value < now) .Select(kvp => kvp.Key) .ToList(); // Remove expired IDs // TryRemove is thread-safe foreach (string id in expiredIds) _ = receivedMessageIds.TryRemove(id, out _); } /// /// Gets the current count of tracked message IDs. /// Useful for monitoring and debugging. /// public int TrackedMessageCount => receivedMessageIds.Count; /// /// Clears all tracked message IDs. /// public void Clear() { receivedMessageIds.Clear(); } /// /// Disposes the message deduplicator and stops the cleanup timer. /// public void Dispose() { if (Interlocked.CompareExchange(ref disposed, 1, 0) == 0) cleanupTimer?.Dispose(); GC.SuppressFinalize(this); } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/LANPlayerManager.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Net; using DTAClient.Domain.Multiplayer.LAN; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer; /// /// Thread-safe manager for LAN lobby players. /// Encapsulates all player tracking operations to ensure atomicity between /// the player dictionary and UI updates. /// internal class LANPlayerManager { private readonly object lockObject = new(); private readonly Dictionary players = []; private readonly Dictionary usernameToListIndex = []; private readonly XNAListBox playerListBox; /// /// Initializes a new instance of the LANPlayerManager class with the specified player list box. /// /// Note: after passing the XNAListBox, do not modify the XNAListBox directly! Use the methods of this class to ensure thread safety. /// /// The XNAListBox control that displays the list of players in the LAN session. Cannot be null. /// Thrown if playerListBox is null. public LANPlayerManager(XNAListBox playerListBox) { this.playerListBox = playerListBox ?? throw new ArgumentNullException(nameof(playerListBox)); } private static string GetKeyFromEndPoint(IPEndPoint endPoint) => endPoint.ToString(); /// /// Gets or creates a player. Returns the LANLobbyUser instance (either newly created or existing). /// This operation is atomic - both the internal dictionary and UI are updated together. /// /// The endpoint (IP:Port) that uniquely identifies this connection. /// The player's username. /// The game icon texture. /// The LANLobbyUser instance (either newly created or existing). public LANLobbyUser GetOrCreatePlayer(IPEndPoint endPoint, string name, Texture2D gameTexture) { lock (lockObject) { string key = GetKeyFromEndPoint(endPoint); // If this endpoint already exists, return the existing user if (players.TryGetValue(key, out LANLobbyUser? existingUser)) { return existingUser; } // Create new user var newUser = new LANLobbyUser(name, gameTexture, endPoint); players[key] = newUser; // Add to UI if username not already displayed if (!usernameToListIndex.ContainsKey(name)) { // FIXME: This logic allows multiple players with the same username but different endpoints to exist simultaneously. // Only the first player with a given username is shown in the UI. // When that player disconnects, the username is removed from the UI even if other players with the same username are still connected. // This can lead to invisible players. // Consider either enforcing unique usernames or updating the UI tracking to handle multiple players per username correctly. int index = playerListBox.Items.Count; usernameToListIndex[name] = index; playerListBox.AddItem(name, gameTexture); } return newUser; } } /// /// Attempts to get a player by endpoint. /// public LANLobbyUser? GetPlayerIfExist(IPEndPoint endPoint) { lock (lockObject) { string key = GetKeyFromEndPoint(endPoint); _ = players.TryGetValue(key, out LANLobbyUser? user); return user; } } /// /// Removes a player by endpoint. This operation is atomic. /// /// True if the player was removed, false if not found. public bool RemovePlayer(IPEndPoint endPoint) { lock (lockObject) { string key = GetKeyFromEndPoint(endPoint); if (!players.TryGetValue(key, out LANLobbyUser? user)) return false; _ = players.Remove(key); // Check if any other player has the same username bool usernameStillInUse = players.Values.Any(p => p.Name == user.Name); if (!usernameStillInUse && usernameToListIndex.TryGetValue(user.Name, out int index)) { // Remove from UI _ = usernameToListIndex.Remove(user.Name); playerListBox.RemoveItem(index); // Update indices for all usernames that came after the removed one // We need to iterate carefully to avoid modifying the dictionary while iterating List keysToUpdate = usernameToListIndex .Where(kvp => kvp.Value > index) .Select(kvp => kvp.Key) .ToList(); // Apply the updates foreach (string username in keysToUpdate) usernameToListIndex[username]--; } return true; } } /// /// Gets a thread-safe snapshot of all players. /// public List GetAllPlayers() { lock (lockObject) { return players.Values.ToList(); } } /// /// Clears all players from both internal tracking and UI. /// public void Clear() { lock (lockObject) { players.Clear(); usernameToListIndex.Clear(); playerListBox.Clear(); } } /// /// Gets the current player count. /// public int Count { get { lock (lockObject) { return players.Count; } } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/PlayerExtraOptionsPanel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using ClientGUI; using DTAClient.Domain.Multiplayer; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { public class PlayerExtraOptionsPanel : XNAPanel { private const int maxStartCount = 8; private const int defaultX = 24; private const int defaultTeamStartMappingX = UIDesignConstants.EMPTY_SPACE_SIDES; private const int teamMappingPanelWidth = 50; private const int teamMappingPanelHeight = 22; private readonly string customPresetName = "Custom".L10N("Client:Main:CustomPresetName"); private XNAClientCheckBox chkBoxForceRandomSides; private XNAClientCheckBox chkBoxForceNoTeams; private XNAClientCheckBox chkBoxForceRandomColors; private XNAClientCheckBox chkBoxForceRandomStarts; private XNAClientCheckBox chkBoxUseTeamStartMappings; private XNAClientDropDown ddTeamStartMappingPreset; private TeamStartMappingsPanel teamStartMappingsPanel; private bool _isHost; private bool ignoreMappingChanges; public EventHandler OptionsChanged; public EventHandler OnClose; private GameModeMap _gameModeMap; public PlayerExtraOptionsPanel(WindowManager windowManager) : base(windowManager) { } public bool ForcedRandomSides { get => chkBoxForceRandomSides.Checked; set => chkBoxForceRandomSides.Checked = value; } public bool ForcedNoTeams { get => chkBoxForceNoTeams.Checked; set => chkBoxForceNoTeams.Checked = value; } public bool ForcedNoTeamsAllowChecking { get => field; set { field = value; RefreshChkBoxForceNoTeams_AllowChecking(); } } public bool ForcedRandomColors { get => chkBoxForceRandomColors.Checked; set => chkBoxForceRandomColors.Checked = value; } public bool ForcedRandomStarts { get => chkBoxForceRandomStarts.Checked; set => chkBoxForceRandomStarts.Checked = value; } public bool UseTeamStartMappings { get => chkBoxUseTeamStartMappings.Checked; set => chkBoxUseTeamStartMappings.Checked = value; } public bool UseTeamStartMappingsAllowChecking { get => chkBoxUseTeamStartMappings.AllowChecking; set => chkBoxUseTeamStartMappings.AllowChecking = value; } private void Options_Changed(object sender, EventArgs e) => OptionsChanged?.Invoke(sender, e); private void Mapping_Changed(object sender, EventArgs e) { Options_Changed(sender, e); if (ignoreMappingChanges) return; ddTeamStartMappingPreset.SelectedIndex = 0; } private void ChkBoxUseTeamStartMappings_Changed(object sender, EventArgs e) { RefreshTeamStartMappingsPanel(); chkBoxForceNoTeams.Checked = chkBoxForceNoTeams.Checked || chkBoxUseTeamStartMappings.Checked; RefreshChkBoxForceNoTeams_AllowChecking(); RefreshPresetDropdown(); Options_Changed(sender, e); } private void RefreshChkBoxForceNoTeams_AllowChecking() => chkBoxForceNoTeams.AllowChecking = ForcedNoTeamsAllowChecking && !chkBoxUseTeamStartMappings.Checked; private void RefreshTeamStartMappingsPanel() { teamStartMappingsPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked); RefreshTeamStartMappingPanels(); } private void AddLocationAssignments() { for (int i = 0; i < maxStartCount; i++) { var teamStartMappingPanel = new TeamStartMappingPanel(WindowManager, i + 1); teamStartMappingPanel.ClientRectangle = GetTeamMappingPanelRectangle(i); teamStartMappingsPanel.AddMappingPanel(teamStartMappingPanel); } teamStartMappingsPanel.MappingChanged += Mapping_Changed; } private Rectangle GetTeamMappingPanelRectangle(int index) { const int maxColumnCount = 2; const int mappingPanelDefaultX = 4; const int mappingPanelDefaultY = 0; if (index > 0 && index % maxColumnCount == 0) // need to start a new column return new Rectangle(((index / maxColumnCount) * (teamMappingPanelWidth + mappingPanelDefaultX)) + 3, mappingPanelDefaultY, teamMappingPanelWidth, teamMappingPanelHeight); var lastControl = index > 0 ? teamStartMappingsPanel.GetTeamStartMappingPanels()[index - 1] : null; return new Rectangle(lastControl?.X ?? mappingPanelDefaultX, lastControl?.Bottom + 4 ?? mappingPanelDefaultY, teamMappingPanelWidth, teamMappingPanelHeight); } private void ClearTeamStartMappingSelections() => teamStartMappingsPanel.GetTeamStartMappingPanels().ForEach(panel => panel.ClearSelections()); private void RefreshTeamStartMappingPanels() { ClearTeamStartMappingSelections(); var teamStartMappingPanels = teamStartMappingsPanel.GetTeamStartMappingPanels(); for (int i = 0; i < teamStartMappingPanels.Count; i++) { var teamStartMappingPanel = teamStartMappingPanels[i]; teamStartMappingPanel.ClearSelections(); if (!UseTeamStartMappings) continue; teamStartMappingPanel.EnableControls(_isHost && chkBoxUseTeamStartMappings.Checked && _gameModeMap != null && _gameModeMap.AllowedStartingLocations.Contains(i + 1)); RefreshTeamStartMappingPresets(_gameModeMap?.Map?.TeamStartMappingPresets); } } private void RefreshTeamStartMappingPresets(List teamStartMappingPresets) { ddTeamStartMappingPreset.Items.Clear(); ddTeamStartMappingPreset.AddItem(new XNADropDownItem { Text = customPresetName, Tag = new List() }); ddTeamStartMappingPreset.SelectedIndex = 0; if (!(teamStartMappingPresets?.Any() ?? false)) return; teamStartMappingPresets.ForEach(preset => ddTeamStartMappingPreset.AddItem(new XNADropDownItem { Text = preset.Name, Tag = preset.TeamStartMappings })); ddTeamStartMappingPreset.SelectedIndex = 1; } private void DdTeamMappingPreset_SelectedIndexChanged(object sender, EventArgs e) { var selectedItem = ddTeamStartMappingPreset.SelectedItem; if (selectedItem?.Text == customPresetName) return; var teamStartMappings = selectedItem?.Tag as List; ignoreMappingChanges = true; teamStartMappingsPanel.SetTeamStartMappings(teamStartMappings); ignoreMappingChanges = false; } private void RefreshPresetDropdown() => ddTeamStartMappingPreset.AllowDropDown = _isHost && chkBoxUseTeamStartMappings.Checked; public override void Initialize() { Name = nameof(PlayerExtraOptionsPanel); BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 255), 1, 1); Visible = false; var btnClose = new XNAClientButton(WindowManager); btnClose.Name = "btnClose"; btnClose.ClientRectangle = new Rectangle(0, 0, 0, 0); btnClose.IdleTexture = AssetLoader.LoadTexture("optionsButtonClose.png"); btnClose.HoverTexture = AssetLoader.LoadTexture("optionsButtonClose_c.png"); btnClose.LeftClick += (sender, args) => Disable(); AddChild(btnClose); var lblHeader = new XNALabel(WindowManager); lblHeader.Name = "lblHeader"; lblHeader.Text = "Extra Player Options".L10N("Client:Main:ExtraPlayerOptions"); lblHeader.ClientRectangle = new Rectangle(defaultX, 4, 0, 18); AddChild(lblHeader); chkBoxForceRandomSides = new XNAClientCheckBox(WindowManager); chkBoxForceRandomSides.Name = "chkBoxForceRandomSides"; chkBoxForceRandomSides.Text = "Force Random Sides".L10N("Client:Main:ForceRandomSides"); chkBoxForceRandomSides.ClientRectangle = new Rectangle(defaultX, lblHeader.Bottom + 4, 0, 0); chkBoxForceRandomSides.CheckedChanged += Options_Changed; AddChild(chkBoxForceRandomSides); chkBoxForceRandomColors = new XNAClientCheckBox(WindowManager); chkBoxForceRandomColors.Name = "chkBoxForceRandomColors"; chkBoxForceRandomColors.Text = "Force Random Colors".L10N("Client:Main:ForceRandomColors"); chkBoxForceRandomColors.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomSides.Bottom + 4, 0, 0); chkBoxForceRandomColors.CheckedChanged += Options_Changed; AddChild(chkBoxForceRandomColors); chkBoxForceNoTeams = new XNAClientCheckBox(WindowManager); chkBoxForceNoTeams.Name = "chkBoxForceNoTeams"; chkBoxForceNoTeams.Text = "Force No Teams".L10N("Client:Main:ForceNoTeams"); chkBoxForceNoTeams.ClientRectangle = new Rectangle(defaultX, chkBoxForceRandomColors.Bottom + 4, 0, 0); chkBoxForceNoTeams.CheckedChanged += Options_Changed; AddChild(chkBoxForceNoTeams); chkBoxForceRandomStarts = new XNAClientCheckBox(WindowManager); chkBoxForceRandomStarts.Name = "chkBoxForceRandomStarts"; chkBoxForceRandomStarts.Text = "Force Random Starts".L10N("Client:Main:ForceRandomStarts"); chkBoxForceRandomStarts.ClientRectangle = new Rectangle(defaultX, chkBoxForceNoTeams.Bottom + 4, 0, 0); chkBoxForceRandomStarts.CheckedChanged += Options_Changed; AddChild(chkBoxForceRandomStarts); ///////////////////////////// chkBoxUseTeamStartMappings = new XNAClientCheckBox(WindowManager); chkBoxUseTeamStartMappings.Name = "chkBoxUseTeamStartMappings"; chkBoxUseTeamStartMappings.Text = "Enable Auto Allying:".L10N("Client:Main:EnableAutoAllying"); chkBoxUseTeamStartMappings.ClientRectangle = new Rectangle(chkBoxForceRandomSides.X, chkBoxForceRandomStarts.Bottom + 20, 0, 0); chkBoxUseTeamStartMappings.CheckedChanged += ChkBoxUseTeamStartMappings_Changed; AddChild(chkBoxUseTeamStartMappings); var btnHelp = new XNAClientButton(WindowManager); btnHelp.Name = "btnHelp"; btnHelp.IdleTexture = AssetLoader.LoadTexture("questionMark.png"); btnHelp.HoverTexture = AssetLoader.LoadTexture("questionMark_c.png"); btnHelp.LeftClick += BtnHelp_LeftClick; btnHelp.ClientRectangle = new Rectangle(chkBoxUseTeamStartMappings.Right + 4, chkBoxUseTeamStartMappings.Y - 1, 0, 0); AddChild(btnHelp); var lblPreset = new XNALabel(WindowManager); lblPreset.Name = "lblPreset"; lblPreset.Text = "Presets:".L10N("Client:Main:Presets"); lblPreset.ClientRectangle = new Rectangle(chkBoxUseTeamStartMappings.X, chkBoxUseTeamStartMappings.Bottom + 8, 0, 0); AddChild(lblPreset); ddTeamStartMappingPreset = new XNAClientDropDown(WindowManager); ddTeamStartMappingPreset.Name = "ddTeamStartMappingPreset"; ddTeamStartMappingPreset.ClientRectangle = new Rectangle(lblPreset.X + 50, lblPreset.Y - 2, 160, 0); ddTeamStartMappingPreset.SelectedIndexChanged += DdTeamMappingPreset_SelectedIndexChanged; ddTeamStartMappingPreset.AllowDropDown = true; AddChild(ddTeamStartMappingPreset); teamStartMappingsPanel = new TeamStartMappingsPanel(WindowManager); teamStartMappingsPanel.Name = "teamStartMappingsPanel"; teamStartMappingsPanel.ClientRectangle = new Rectangle(lblPreset.X, ddTeamStartMappingPreset.Bottom + 8, Width, Height - ddTeamStartMappingPreset.Bottom + 4); AddChild(teamStartMappingsPanel); AddLocationAssignments(); base.Initialize(); RefreshTeamStartMappingsPanel(); } private void BtnHelp_LeftClick(object sender, EventArgs args) { XNAMessageBox.Show(WindowManager, "Auto Allying".L10N("Client:Main:AutoAllyingTitle"), ("Auto allying allows the host to assign starting locations to teams, not players.\n" + "When players are assigned to spawn locations, they will be auto assigned to teams based on these mappings.\n" + "This is best used with random teams and random starts. However, only random teams is required.\n" + "Manually specified starts will take precedence.").L10N("Client:Main:AutoAllyingText1") + "\n\n" + $"{TeamStartMapping.NO_PLAYER} : " + "Block this location from being randomly assigned to a player if there are spare locations.".L10N("Client:Main:AutoAllyingTextNoPlayerV2") + "\n" + $"{TeamStartMapping.NO_TEAM} : " + "Allow a player here, but don't assign a team.".L10N("Client:Main:AutoAllyingTextNoTeamV2") ); } public void UpdateForGameModeMap(GameModeMap gameModeMap) { if (_gameModeMap == gameModeMap) return; _gameModeMap = gameModeMap; RefreshTeamStartMappingPanels(); } public List GetTeamStartMappings() => chkBoxUseTeamStartMappings.Checked ? teamStartMappingsPanel.GetTeamStartMappings() : new List(); public void EnableControls(bool enable) { chkBoxForceRandomSides.InputEnabled = enable; chkBoxForceRandomColors.InputEnabled = enable; chkBoxForceRandomStarts.InputEnabled = enable; chkBoxForceNoTeams.InputEnabled = enable; chkBoxUseTeamStartMappings.InputEnabled = enable; teamStartMappingsPanel.EnableControls(enable && chkBoxUseTeamStartMappings.Checked); } public PlayerExtraOptions GetPlayerExtraOptions() => new PlayerExtraOptions() { IsForceRandomSides = ForcedRandomSides, IsForceRandomColors = ForcedRandomColors, IsForceRandomStarts = ForcedRandomStarts, IsForceNoTeams = ForcedNoTeams, IsUseTeamStartMappings = UseTeamStartMappings, TeamStartMappings = GetTeamStartMappings() }; public void SetPlayerExtraOptions(PlayerExtraOptions playerExtraOptions) { chkBoxForceRandomSides.Checked = playerExtraOptions.IsForceRandomSides; chkBoxForceRandomColors.Checked = playerExtraOptions.IsForceRandomColors; chkBoxForceNoTeams.Checked = playerExtraOptions.IsForceNoTeams; chkBoxForceRandomStarts.Checked = playerExtraOptions.IsForceRandomStarts; chkBoxUseTeamStartMappings.Checked = playerExtraOptions.IsUseTeamStartMappings; teamStartMappingsPanel.SetTeamStartMappings(playerExtraOptions.TeamStartMappings); } public void SetIsHost(bool isHost) { _isHost = isHost; RefreshPresetDropdown(); EnableControls(_isHost); } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/PlayerListBox.cs ================================================ using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Online; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System.Collections.Generic; using System.IO; using System.Reflection; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer { /// /// A list box for listing the players in the CnCNet lobby. /// public class PlayerListBox : XNAListBox { private const int MARGIN = 2; public List Users; private Texture2D adminGameIcon; private Texture2D unknownGameIcon; private Texture2D friendIcon; private Texture2D ignoreIcon; private Texture2D? voiceIcon; private GameCollection gameCollection; public PlayerListBox(WindowManager windowManager, GameCollection gameCollection) : base(windowManager) { this.gameCollection = gameCollection; Users = new List(); var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream cncnetIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.cncneticon.png"); using Stream unknownIconStream = assembly.GetManifestResourceStream("DTAClient.Icons.unknownicon.png"); adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream)); unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); friendIcon = AssetLoader.LoadTexture("friendicon.png"); ignoreIcon = AssetLoader.LoadTexture("ignoreicon.png"); const string voiceIconName = "voiceicon.png"; if (AssetLoader.AssetExists(voiceIconName)) voiceIcon = AssetLoader.LoadTexture(voiceIconName); } public void AddUser(ChannelUser user) { XNAListBoxItem item = new XNAListBoxItem(); UpdateItemInfo(user, item); AddItem(item); } public void UpdateUserInfo(ChannelUser user) { XNAListBoxItem item = Items.Find(x => x.Tag == user); UpdateItemInfo(user, item); } public override void Draw(GameTime gameTime) { DrawPanel(); int height = 2 - (ViewTop % LineHeight); for (int i = TopIndex; i < Items.Count; i++) { XNAListBoxItem lbItem = Items[i]; var user = (ChannelUser)lbItem.Tag; if (height > Height) break; int x = TextBorderDistance; if (i == SelectedIndex) { int drawnWidth; if (DrawSelectionUnderScrollbar || !ScrollBar.IsDrawn() || !EnableScrollbar) { drawnWidth = Width - 2; } else { drawnWidth = Width - 2 - ScrollBar.Width; } FillRectangle(new Rectangle(1, height, drawnWidth, lbItem.TextLines.Count * LineHeight), FocusColor); } DrawTexture(user.IsAdmin ? adminGameIcon : lbItem.Texture, new Rectangle(x, height, adminGameIcon.Width, adminGameIcon.Height), Color.White); x += adminGameIcon.Width + MARGIN; // Friend Icon if (user.IRCUser.IsFriend) { DrawTexture(friendIcon, new Rectangle(x, height, friendIcon.Width, friendIcon.Height), Color.White); x += friendIcon.Width + MARGIN; } // Ignore Icon else if (user.IRCUser.IsIgnored && !user.IsAdmin) { DrawTexture(ignoreIcon, new Rectangle(x, height, ignoreIcon.Width, ignoreIcon.Height), Color.White); x += ignoreIcon.Width + MARGIN; } // Voice Icon if (user.HasVoice && voiceIcon != null) { DrawTexture(voiceIcon, new Rectangle(x, height, voiceIcon.Width, voiceIcon.Height), Color.White); x += voiceIcon.Width + MARGIN; } // Player Name string name = user.IsAdmin ? user.IRCUser.Name + " " + "(Admin)".L10N("Client:Main:AdminSuffix") : user.IRCUser.Name; x += lbItem.TextXPadding; DrawStringWithShadow(name, FontIndex, new Vector2(x, height), user.IsAdmin ? Color.Red : lbItem.TextColor); height += LineHeight; } if (DrawBorders) DrawPanelBorders(); DrawChildren(gameTime); } private void UpdateItemInfo(ChannelUser user, XNAListBoxItem item) { item.Tag = user; if (user.IsAdmin) { item.Text = user.IRCUser.Name + " " + "(Admin)".L10N("Client:Main:AdminSuffix"); item.TextColor = Color.Red; item.Texture = adminGameIcon; } else { item.Text = user.IRCUser.Name; if (user.IRCUser.GameID < 0 || user.IRCUser.GameID >= gameCollection.GameList.Count) item.Texture = unknownGameIcon; else item.Texture = gameCollection.GameList[user.IRCUser.GameID].Texture; } } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/TeamStartMappingPanel.cs ================================================ using System; using ClientGUI; using DTAClient.Domain.Multiplayer; using Microsoft.Xna.Framework; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { public class TeamStartMappingPanel : XNAPanel { private readonly int _start; private readonly int _defaultTeamIndex = -1; private const int ddWidth = 35; // private XNAClientDropDown ddStarts; private XNAClientDropDown ddTeams; public event EventHandler OptionsChanged; public TeamStartMappingPanel(WindowManager windowManager, int start) : base(windowManager) { _start = start; DrawBorders = false; } public override void Initialize() { base.Initialize(); var startLabel = new XNALabel(WindowManager); startLabel.Text = _start.ToString(); startLabel.ClientRectangle = new Rectangle(0, 0, 10, 22); AddChild(startLabel); ddTeams = new XNAClientDropDown(WindowManager); ddTeams.Name = nameof(ddTeams); ddTeams.ClientRectangle = new Rectangle(startLabel.Right, startLabel.Y - 3, ddWidth, 22); TeamStartMapping.TEAMS.ForEach(ddTeams.AddItem); AddChild(ddTeams); ddTeams.SelectedIndexChanged += DD_SelectedItemChanged; } private void DD_SelectedItemChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(sender, e); public void SetTeamStartMapping(TeamStartMapping teamStartMapping) { var teamIndex = teamStartMapping?.TeamIndex ?? _defaultTeamIndex; ddTeams.SelectedIndex = teamIndex >= 0 && teamIndex < ddTeams.Items.Count ? teamIndex : -1; } public void EnableControls(bool enable) => ddTeams.AllowDropDown = enable; public void ClearSelections() => ddTeams.SelectedIndex = _defaultTeamIndex; public TeamStartMapping GetTeamStartMapping() { return new TeamStartMapping() { Team = ddTeams.SelectedItem?.Text, Start = _start }; } } } ================================================ FILE: DXMainClient/DXGUI/Multiplayer/TeamStartMappingsPanel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using DTAClient.Domain.Multiplayer; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; namespace DTAClient.DXGUI.Multiplayer { public class TeamStartMappingsPanel : XNAPanel { public event EventHandler MappingChanged; public TeamStartMappingsPanel(WindowManager windowManager) : base(windowManager) { DrawBorders = false; } public List GetTeamStartMappingPanels() => Children.Select(c => c as TeamStartMappingPanel).ToList(); public void EnableControls(bool enable) => GetTeamStartMappingPanels().ForEach(panel => panel.EnableControls(enable)); public List GetTeamStartMappings() { return GetTeamStartMappingPanels() .Select(panel => panel.GetTeamStartMapping()) .Where(mapping => mapping.IsValid) .ToList(); } public void AddMappingPanel(TeamStartMappingPanel teamStartMappingPanel) { teamStartMappingPanel.OptionsChanged += (sender, args) => MappingChanged?.Invoke(sender, args); AddChild(teamStartMappingPanel); } public void SetTeamStartMappings(List teamStartMappings) { var teamStartMappingPanels = GetTeamStartMappingPanels(); for (int i = 0; i < teamStartMappingPanels.Count; i++) { if (teamStartMappings.Count <= i) { teamStartMappingPanels[i].ClearSelections(); continue; } var teamStartMapping = teamStartMappings[i]; teamStartMappingPanels[i].SetTeamStartMapping(teamStartMapping); } } } } ================================================ FILE: DXMainClient/DXMainClient.csproj ================================================  WinExe Exe true false win-x86 false false false CnCNet Main Client CnCNet Client DTAClient clienticon.ico SystemAware PerMonitorV2 clientdx clientogl clientxna true $(NoWarn);WFAC010;WFO0003 app.SystemAware.manifest app.PerMonitorV2.manifest Always ================================================ FILE: DXMainClient/Domain/CustomMissionHelper.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; namespace DTAClient.Domain; internal static class CustomMissionHelper { public static List<(string extension, string filename)>? CustomMissionSupplementDefinition { get; private set; } private static bool IsValidExtension(string extension) => extension == extension.ToWin32FileName() && extension.IndexOfAny(new char[] { '.', ' ' }) == -1; private static bool IsValidFileName(string filename) => filename == filename.ToWin32FileName(); public static void Initialize() { CustomMissionSupplementDefinition = GetCustomMissionSupplementDefinition(); } public static List<(string extension, string filename)> GetCustomMissionSupplementDefinition() { List<(string extension, string copyAs)> configFiles = ClientConfiguration.Instance.GetCustomMissionSupplementFiles(); HashSet extensions = new HashSet(StringComparer.OrdinalIgnoreCase); List<(string extension, string filename)> ret = []; foreach ((string extension, string filename) in configFiles) { if (!IsValidExtension(extension)) { throw new Exception(string.Format("Invalid extension {0}", extension)); } if (!IsValidFileName(filename)) { throw new Exception(string.Format("Invalid file name {0}", filename)); } if (extensions.Contains(extension)) { throw new Exception(string.Format("Extension {0} already exists", extension)); } extensions.Add(extension); ret.Add((extension, filename)); } return ret; } public static void DeleteSupplementalMissionFiles() { Debug.Assert(CustomMissionSupplementDefinition != null, "CustomMissionHelper must be initialized."); IEnumerable filenames = CustomMissionSupplementDefinition.Select(def => def.filename); DirectoryInfo gameDirectory = SafePath.GetDirectory(ProgramConstants.GamePath); foreach (string filename in filenames) { FileInfo? fileInfo = gameDirectory.EnumerateFiles(filename).SingleOrDefault(); if (fileInfo?.Exists ?? false) { fileInfo.IsReadOnly = false; fileInfo.Delete(); } } } public static void CopySupplementalMissionFiles(Mission mission) { Debug.Assert(CustomMissionSupplementDefinition != null, "CustomMissionHelper must be initialized."); DeleteSupplementalMissionFiles(); if (mission.IsCustomMission) { string mapExtension = "." + ClientConfiguration.Instance.MapFileExtension; // e.g., ".map" string missionFileName = mission.Scenario; Debug.Assert(missionFileName.EndsWith(mapExtension, StringComparison.InvariantCultureIgnoreCase), string.Format("Mission file should have the extension \"{0}\".", mapExtension)); // copy the CSF file if exists foreach ((string ext, string filename) in CustomMissionSupplementDefinition!) { string sourceFileName = missionFileName[..^mapExtension.Length] + "." + ext; string sourceFilePath = SafePath.CombineFilePath(ProgramConstants.GamePath, sourceFileName); if (SafePath.GetFile(sourceFilePath).Exists) { string targetFilePath = SafePath.CombineFilePath(ProgramConstants.GamePath, filename); FileExtensions.CreateHardLinkFromSource(sourceFilePath, targetFilePath); new FileInfo(targetFilePath).IsReadOnly = true; } } } } } ================================================ FILE: DXMainClient/Domain/DirectDrawCompatibilityChecker.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Versioning; using ClientCore; using ClientCore.Extensions; using ClientGUI; using Microsoft.Win32; using Rampastring.Tools; using Rampastring.XNAUI; namespace DTAClient.Domain; /// /// Handles checking and fixing DirectDraw compatibility issues with user interaction. /// [SupportedOSPlatform("windows")] public static class DirectDrawCompatibilityChecker { private static readonly IReadOnlyList OSCompatibilityValues = [ "WIN8RTM", "WIN7RTM", "VISTASP2", "VISTASP1", "VISTARTM", "WINXPSP3", "WINXPSP2", "WIN98", "WIN95" ]; private static IEnumerable GetExecutableFilePathsToCheck() { List executablePaths = ClientConfiguration.Instance.GetCompatibilityCheckExecutables() .Select(executableName => SafePath.CombineFilePath(ProgramConstants.GamePath, executableName)) .ToList(); // clientdx.exe, clientogl.exe, or clientxna.exe string currentExePath = SafePath.GetFile(ProgramConstants.StartupExecutable).FullName; executablePaths.Add(currentExePath); Logger.Log("Checking compatibility settings for executables: " + string.Join(", ", executablePaths)); return executablePaths; } private static void Examine(out bool requireFix, out bool requireAdmin, out IEnumerable problematicExeNames) { RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); RegistryKey hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64); using RegistryKey? hkcuKey = hkcu.OpenSubKey( @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"); using RegistryKey? hklmKey = hklm.OpenSubKey( @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"); static bool IsFixRequired(object? regValue) => regValue is string regValueString && regValueString.Split([' ']).Intersect(OSCompatibilityValues).Any(); bool anyHkcuRequireFix = false; bool anyHklmRequireFix = false; var problematicExeNameHashSet = new HashSet(); foreach (string exeFullPath in GetExecutableFilePathsToCheck()) { object? hkcuValue = hkcuKey?.GetValue(exeFullPath); object? hklmValue = hklmKey?.GetValue(exeFullPath); if (IsFixRequired(hkcuValue)) { Logger.Log($"Executable '{exeFullPath}' has problematic compatibility settings in HKCU. Value: {hkcuValue}"); anyHkcuRequireFix = true; problematicExeNameHashSet.Add(Path.GetFileName(exeFullPath)); } if (IsFixRequired(hklmValue)) { Logger.Log($"Executable '{exeFullPath}' has problematic compatibility settings in HKLM. Value: {hklmValue}"); anyHklmRequireFix = true; problematicExeNameHashSet.Add(Path.GetFileName(exeFullPath)); } } requireFix = anyHkcuRequireFix || anyHklmRequireFix; requireAdmin = anyHklmRequireFix; problematicExeNames = problematicExeNameHashSet; } private static string FixCompatLayerString(string value) => string.Join(" ", value .SplitWithCleanup(new[] { ' ' }) .Where(v => !OSCompatibilityValues.Contains(v, StringComparer.InvariantCultureIgnoreCase))); private static void Fix() { void FixRegValue(object? regValue, out bool success, out string newRegValue) { if (regValue is string regValueString) { newRegValue = FixCompatLayerString(regValueString); success = true; } else { success = false; newRegValue = string.Empty; } } void FixRegistryKey(RegistryKey rootKey, string subKeyPath) { try { using RegistryKey? key = rootKey.OpenSubKey(subKeyPath, writable: true); if (key == null) return; foreach (string exeFullPath in GetExecutableFilePathsToCheck()) { object? value = key.GetValue(exeFullPath); FixRegValue(value, out bool success, out string newValue); if (success) { if (string.IsNullOrEmpty(newValue)) key.DeleteValue(exeFullPath, false); else key.SetValue(exeFullPath, newValue, RegistryValueKind.String); } } } catch (Exception ex) { Logger.Log($"Failed to fix registry key {rootKey.Name}\\{subKeyPath}: {ex.Message}"); } } string subKeyPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"; RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); RegistryKey hkcu = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64); FixRegistryKey(hkcu, subKeyPath); FixRegistryKey(hklm, subKeyPath); } /// /// Checks for DirectDraw compatibility issues and prompts the user to fix them. /// /// The WindowManager for displaying message boxes. public static void CheckAndPromptFix(WindowManager windowManager) { // Fix environment variable __COMPAT_LAYER first, for the client itself. string compatLayerEnv = Environment.GetEnvironmentVariable("__COMPAT_LAYER") ?? string.Empty; string fixedCompatLayerEnv = FixCompatLayerString(compatLayerEnv); if (compatLayerEnv != fixedCompatLayerEnv) { Logger.Log("Fixing __COMPAT_LAYER environment variable. Previous value: " + $"'{compatLayerEnv}', new value: '{fixedCompatLayerEnv}'"); Environment.SetEnvironmentVariable("__COMPAT_LAYER", fixedCompatLayerEnv); } // Now check registry compatibility settings for all relevant executables. try { Examine(out bool requireFix, out bool requireAdmin, out var problematicExeNames); if (!requireFix) return; Logger.Log("DirectDraw compatibility issue detected."); string localizedMessage = "Problematic Windows compatibility mode settings have been detected that may interfere with the game." .L10N("Client:Main:ProblematicCompatibilityText1") + "\n\n" + "Affected executables:".L10N("Client:Main:ProblematicCompatibilityText2") + "\n- " + string.Join("\n- ", problematicExeNames) + "\n\n" + "Would you like to remove these compatibility settings now?".L10N("Client:Main:ProblematicCompatibilityText3"); if (requireAdmin && !AdminRestarter.IsRunningAsAdministrator()) { localizedMessage += "\n\n" + ("Note: Administrator privileges are required to remove compatibility settings." + " " + "Clicking Yes will relaunch the client with administrator permissions.").L10N("Client:Main:ProblematicCompatibilityText4"); } var messageBox = XNAMessageBox.ShowYesNoDialog(windowManager, "Problematic Compatibility Settings Detected".L10N("Client:Main:ProblematicCompatibilityTitle"), localizedMessage); messageBox.YesClickedAction = _ => { if (requireAdmin && !AdminRestarter.IsRunningAsAdministrator()) { Logger.Log("Administrator privileges required. Restart with elevated privileges."); if (AdminRestarter.RestartAsAdmin()) windowManager.CloseGame(); } else { Logger.Log("Attempting to fix DirectDraw compatibility settings."); Fix(); Logger.Log("DirectDraw compatibility settings fixed successfully."); XNAMessageBox.Show(windowManager, "Fix Applied".L10N("Client:Main:CompatibilityFixAppliedTitle"), "Compatibility settings have been removed successfully.".L10N("Client:Main:CompatibilityFixAppliedText")); } }; messageBox.NoClickedAction = _ => { Logger.Log("User declined to fix DirectDraw compatibility settings."); }; } catch (Exception ex) { Logger.Log("Error checking DirectDraw compatibility: " + ex.ToString()); } } } ================================================ FILE: DXMainClient/Domain/DirectDrawWrapper.cs ================================================ using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace DTAClient.Domain { /// /// A DirectDraw wrapper option. /// public class DirectDrawWrapper { /// /// Creates a new DirectDrawWrapper instance and parses its configuration /// from an INI file. /// /// The internal name of the renderer. /// The file to parse the renderer's options from. public DirectDrawWrapper(string internalName, IniFile iniFile) { InternalName = internalName; Parse(iniFile.GetSection(InternalName)); } public string InternalName { get; private set; } public string UIName { get; private set; } /// /// If not null or empty, windowed mode will be written to an INI key /// in this section of the renderer settings file instead /// of the regular game settings INI file. /// public string WindowedModeSection { get; private set; } /// /// If not null or empty, windowed mode will be written to this INI key /// in the section defined in /// instead of the regular settings INI file. /// public string WindowedModeKey { get; private set; } /// /// If not null or empty, the setting that controls whether the game is /// run in borderless windowed mode will be written to this INI key in /// the section defined by /// instead of the /// regular settings INI file. /// public string BorderlessWindowedModeKey { get; private set; } /// /// If set, borderless mode is enabled if the setting is "false" /// and disabled if the setting is "true". /// public bool IsBorderlessWindowedModeKeyReversed { get; private set; } public bool Hidden { get; private set; } /// /// Many ddraw wrappers need qres.dat to set the desktop to 16 bit mode /// public bool UseQres { get; private set; } = true; /// /// If set to false, the client won't set single-core affinity /// to the game executable when this renderer is used. /// public bool SingleCoreAffinity { get; private set; } = true; /// /// The filename of the configuration INI of the renderer in the game directory. /// public string ConfigFileName { get; private set; } /// /// Indicates whether this DirectDrawWrapper is a dummy wrapper (i.e. no wrapper). /// public bool IsDummy => string.IsNullOrEmpty(ddrawDLLPath); private string ddrawDLLPath; private string resConfigFileName; private List filesToCopy = new List(); private List disallowedOSList = new List(); /// /// Reads the properties of this DirectDrawWrapper from an INI section. /// /// The INI section. private void Parse(IniSection section) { if (section == null) { Logger.Log("DirectDrawWrapper: Configuration for renderer '" + InternalName + "' not found!"); return; } UIName = section.GetStringValue("UIName", "Unnamed renderer"); if (section.GetBooleanValue("IsDxWnd", false)) { // For backwards compatibility with previous client versions WindowedModeSection = "DxWnd"; WindowedModeKey = "RunInWindow"; BorderlessWindowedModeKey = "NoWindowFrame"; } WindowedModeSection = section.GetStringValue("WindowedModeSection", WindowedModeSection); WindowedModeKey = section.GetStringValue("WindowedModeKey", WindowedModeKey); BorderlessWindowedModeKey = section.GetStringValue("BorderlessWindowedModeKey", BorderlessWindowedModeKey); IsBorderlessWindowedModeKeyReversed = section.GetBooleanValue("IsBorderlessWindowedModeKeyReversed", IsBorderlessWindowedModeKeyReversed); if (BorderlessWindowedModeKey != null && WindowedModeSection == null) { throw new DirectDrawWrapperConfigurationException( "BorderlessWindowedModeKey= is defined for renderer" + $" {InternalName} but WindowedModeSection= is not!"); } Hidden = section.GetBooleanValue("Hidden", false); UseQres = section.GetBooleanValue("UseQres", UseQres); SingleCoreAffinity = section.GetBooleanValue("SingleCoreAffinity", SingleCoreAffinity); ddrawDLLPath = section.GetStringValue("DLLName", string.Empty); ConfigFileName = section.GetStringValue("ConfigFileName", string.Empty); resConfigFileName = section.GetStringValue("ResConfigFileName", ConfigFileName); filesToCopy = section.GetStringValue("AdditionalFiles", string.Empty).Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); string[] disallowedOSs = section.GetStringValue("DisallowedOperatingSystems", string.Empty).Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string os in disallowedOSs) { OSVersion disallowedOS = (OSVersion)Enum.Parse(typeof(OSVersion), os.Trim()); disallowedOSList.Add(disallowedOS); } if (!string.IsNullOrEmpty(ddrawDLLPath) && !SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), ddrawDLLPath).Exists) Logger.Log("DirectDrawWrapper: File specified in DLLPath= for renderer '" + InternalName + "' does not exist!"); if (!string.IsNullOrEmpty(resConfigFileName) && !SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), resConfigFileName).Exists) Logger.Log("DirectDrawWrapper: File specified in ConfigFileName= for renderer '" + InternalName + "' does not exist!"); foreach (var file in filesToCopy) { if (!SafePath.GetFile(ProgramConstants.GetBaseResourcePath(), file).Exists) Logger.Log("DirectDrawWrapper: Additional file '" + file + "' for renderer '" + InternalName + "' does not exist!"); } } /// /// Returns true if this wrapper is compatible with the given operating /// system, otherwise false. /// /// The operating system. public bool IsCompatibleWithOS(OSVersion os) { return !disallowedOSList.Contains(os); } /// /// Applies the renderer's files to the game directory. /// public void Apply() { string ddrawDllSourcePath = SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ddrawDLLPath); string ddrawDllTargetPath = SafePath.CombineFilePath(ProgramConstants.GamePath, "ddraw.dll"); if (!string.IsNullOrEmpty(ddrawDLLPath)) { FileExtensions.CreateHardLinkFromSource(ddrawDllSourcePath, ddrawDllTargetPath); new FileInfo(ddrawDllSourcePath).IsReadOnly = true; new FileInfo(ddrawDllTargetPath).IsReadOnly = true; } else { if (File.Exists(ddrawDllTargetPath)) { new FileInfo(ddrawDllTargetPath).IsReadOnly = false; File.Delete(ddrawDllTargetPath); } } if (!string.IsNullOrEmpty(ConfigFileName) && !string.IsNullOrEmpty(resConfigFileName) && !SafePath.GetFile(ProgramConstants.GamePath, ConfigFileName).Exists) // Do not overwrite settings { File.Copy(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), resConfigFileName), SafePath.CombineFilePath(ProgramConstants.GamePath, Path.GetFileName(ConfigFileName))); } foreach (var file in filesToCopy) { File.Copy(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), file), SafePath.CombineFilePath(ProgramConstants.GamePath, Path.GetFileName(file)), true); } } /// /// Call to clean the renderer's files from the game directory. /// public void Clean() { if (!string.IsNullOrEmpty(ConfigFileName)) SafePath.DeleteFileIfExists(ProgramConstants.GamePath, Path.GetFileName(ConfigFileName)); foreach (var file in filesToCopy) SafePath.DeleteFileIfExists(ProgramConstants.GamePath, Path.GetFileName(file)); } /// /// Checks whether this renderer enables windowed mode through its /// own configuration INI file instead of the game settings INI file. /// public bool UsesCustomWindowedOption() { return !string.IsNullOrEmpty(WindowedModeSection) && !string.IsNullOrEmpty(WindowedModeKey); } // Override == and != operators to compare by InternalName public static bool operator ==(DirectDrawWrapper a, DirectDrawWrapper b) { if (ReferenceEquals(a, b)) return true; if (a is null || b is null) return false; return a.InternalName == b.InternalName; } public static bool operator !=(DirectDrawWrapper a, DirectDrawWrapper b) => !(a == b); public override bool Equals(object obj) { if (obj is DirectDrawWrapper other) { return this == other; } return false; } public override int GetHashCode() => InternalName.GetHashCode(); } /// /// An exception that is thrown when configuration for DirectDraw wrapper contains /// invalid or unexpected settings / data or required settings / data are missing. /// class DirectDrawWrapperConfigurationException : Exception { public DirectDrawWrapperConfigurationException(string message) : base(message) { } } } ================================================ FILE: DXMainClient/Domain/DirectDrawWrapperManager.cs ================================================ #nullable enable using System.Collections.Generic; using System.Linq; using ClientCore; using ClientGUI; using Rampastring.Tools; namespace DTAClient.Domain { public class DirectDrawWrapperManager { private const string RENDERERS_INI = "Renderers.ini"; private List renderers; private string defaultRenderer; private DirectDrawWrapper selectedRenderer; public DirectDrawWrapper SelectedRenderer => selectedRenderer; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor public DirectDrawWrapperManager() { // This method sets up `renderers`, `defaultRenderer`, and `selectedRenderer` RefreshRenderers(); } #pragma warning restore CS8618 public IEnumerable GetRenderers(OSVersion localOS) => renderers.Where(r => r.IsCompatibleWithOS(localOS) && !r.Hidden); private void RefreshRenderers() { renderers = new List(); var renderersIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), RENDERERS_INI)); var keys = renderersIni.GetSectionKeys("Renderers"); if (keys == null) throw new ClientConfigurationException("[Renderers] not found from Renderers.ini!"); foreach (string key in keys) { string internalName = renderersIni.GetStringValue("Renderers", key, string.Empty); var ddWrapper = new DirectDrawWrapper(internalName, renderersIni); renderers.Add(ddWrapper); } OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); defaultRenderer = renderersIni.GetStringValue("DefaultRenderer", osVersion.ToString(), string.Empty); if (string.IsNullOrEmpty(defaultRenderer)) throw new ClientConfigurationException("Invalid or missing default renderer for operating system: " + osVersion); string renderer = UserINISettings.Instance.Renderer; selectedRenderer = renderers.Find(r => r.InternalName == renderer) ?? renderers.Find(r => r.InternalName == defaultRenderer) ?? throw new ClientConfigurationException("Missing renderer: " + renderer); GameProcessLogic.UseQres = selectedRenderer.UseQres; GameProcessLogic.SingleCoreAffinity = selectedRenderer.SingleCoreAffinity; } public void Save(DirectDrawWrapper? newSelectedRenderer) { var originalRenderer = selectedRenderer; selectedRenderer = newSelectedRenderer ?? originalRenderer; if (selectedRenderer != originalRenderer || !SafePath.GetFile(ProgramConstants.GamePath, selectedRenderer.ConfigFileName).Exists) { foreach (var renderer in renderers.Where(renderer => renderer != selectedRenderer)) { renderer.Clean(); } } selectedRenderer.Apply(); GameProcessLogic.UseQres = selectedRenderer.UseQres; GameProcessLogic.SingleCoreAffinity = selectedRenderer.SingleCoreAffinity; UserINISettings.Instance.Renderer.Value = selectedRenderer.InternalName; } } } ================================================ FILE: DXMainClient/Domain/DiscordHandler.cs ================================================ using System; using ClientCore; using DiscordRPC; using DiscordRPC.Message; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System.Text.RegularExpressions; namespace DTAClient.Domain { /// /// A class for handling Discord integration. /// public class DiscordHandler: IDisposable { private const int MaxDiscordPresenceTextLength = 128; private DiscordRpcClient client; private RichPresence _currentPresence; /// /// RichPresence instance that is currently being displayed. /// public RichPresence CurrentPresence { get { return _currentPresence; } set { if (_currentPresence == null || !_currentPresence.Equals(PreviousPresence)) { PreviousPresence = _currentPresence; _currentPresence = value; client?.SetPresence(_currentPresence); } } } /// /// RichPresence instance that was last displayed before the current one. /// public RichPresence PreviousPresence { get; private set; } /// /// Creates a new instance of Discord handler. /// public DiscordHandler() { if (!UserINISettings.Instance.DiscordIntegration || ClientConfiguration.Instance.DiscordIntegrationGloballyDisabled) return; InitializeClient(); UpdatePresence(); Connect(); AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose(); } #region overrides #endregion #region methods /// /// Initializes or reinitializes Discord RPC client object & event handlers. /// private void InitializeClient() { if (client != null && client.IsInitialized) { client.ClearPresence(); client.Dispose(); client = null; } client = new DiscordRpcClient(ClientConfiguration.Instance.DiscordAppId); client.OnReady += OnReady; client.OnClose += OnClose; client.OnError += OnError; client.OnConnectionEstablished += OnConnectionEstablished; client.OnConnectionFailed += OnConnectionFailed; client.OnPresenceUpdate += OnPresenceUpdate; client.OnSubscribe += OnSubscribe; client.OnUnsubscribe += OnUnsubscribe; if (CurrentPresence != null) client.SetPresence(CurrentPresence); } /// /// Connects to Discord. /// Does not do anything if the Discord RPC client has not been initialized or is already connected. /// public void Connect() { if (client == null || client.IsInitialized) return; bool success = client.Initialize(); if (success) Logger.Log("DiscordHandler: Connected Discord RPC client."); else Logger.Log("DiscordHandler: Failed to connect Discord RPC client."); } /// /// Disconnects from Discord. /// Does not do anything if the Discord RPC client has not been initialized or is not connected. /// public void Disconnect() { if (client == null || !client.IsInitialized) return; // HACK warning // Currently DiscordRpcClient does not appear to have any way to reliably disconnect and reconnect using same client object. // Deinitialize does not appear to completely reset connection state & resources and any attempts to call Initialize afterwards will fail. // A hacky solution is to dispose current client object and create and initialize a new one. InitializeClient(); //client.Deinitialize(); Logger.Log("DiscordHandler: Disconnected Discord RPC client."); } /// /// Updates Discord Rich Presence with default info. /// public void UpdatePresence() { CurrentPresence = new RichPresence() { Details = "In Client", Assets = new Assets() { LargeImageKey = "logo" } }; } /// /// Updates Discord Rich Presence with info from game lobbies. /// public void UpdatePresence(string map, string mode, string type, string state, int players, int maxPlayers, string side, string roomName, bool isHost = false, bool isPassworded = false, bool isLocked = false, bool resetTimer = false) { string sideKey = new Regex("[^a-zA-Z0-9]").Replace(side.ToLower(), ""); string stateString = $"{state} [{players}/{maxPlayers}] • {roomName}"; if (isHost) stateString += "👑"; if (isPassworded) stateString += "🔑"; if (isLocked) stateString += "🔒"; CurrentPresence = new RichPresence() { State = TrimDiscordPresenceText(stateString), Details = TrimDiscordPresenceText($"{type} • {map} • {mode}"), Assets = new Assets() { LargeImageKey = "logo", SmallImageKey = sideKey, SmallImageText = TrimDiscordPresenceText(side) }, Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ? client.CurrentPresence.Timestamps : Timestamps.Now }; } /// /// Updates Discord Rich Presence with info from game loading lobbies. /// public void UpdatePresence(string map, string mode, string type, string state, int players, int maxPlayers, string roomName, bool isHost = false, bool resetTimer = false) { string stateString = $"{state} [{players}/{maxPlayers}] • {roomName}"; stateString += "💾"; if (isHost) stateString += "👑"; CurrentPresence = new RichPresence() { State = TrimDiscordPresenceText(stateString), Details = TrimDiscordPresenceText($"{type} • {map} • {mode}"), Assets = new Assets() { LargeImageKey = "logo" }, Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ? client.CurrentPresence.Timestamps : Timestamps.Now }; } /// /// Updates Discord Rich Presence with info from skirmish "lobby". /// public void UpdatePresence(string map, string mode, string state, string side, bool resetTimer = false) { string sideKey = new Regex("[^a-zA-Z0-9]").Replace(side.ToLower(), ""); CurrentPresence = new RichPresence() { State = TrimDiscordPresenceText(state), Details = TrimDiscordPresenceText($"Skirmish • {map} • {mode}"), Assets = new Assets() { LargeImageKey = "logo", SmallImageKey = sideKey, SmallImageText = TrimDiscordPresenceText(side) }, Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ? client.CurrentPresence.Timestamps : Timestamps.Now }; } /// /// Updates Discord Rich Presence with info from campaign screen. /// public void UpdatePresence(string mission, string difficulty, string side, bool resetTimer = false) { string sideKey = new Regex("[^a-zA-Z0-9]").Replace(side.ToLower(), ""); CurrentPresence = new RichPresence() { State = "Playing Mission", Details = TrimDiscordPresenceText($"{mission} • {difficulty}"), Assets = new Assets() { LargeImageKey = "logo", SmallImageKey = sideKey, SmallImageText = TrimDiscordPresenceText(side) }, Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ? client.CurrentPresence.Timestamps : Timestamps.Now }; } /// /// Updates Discord Rich Presence with info from game loading screen. /// public void UpdatePresence(string save, bool resetTimer = false) { CurrentPresence = new RichPresence() { State = "Playing Saved Game", Details = TrimDiscordPresenceText(save), Assets = new Assets() { LargeImageKey = "logo" }, Timestamps = (client?.CurrentPresence.HasTimestamps() ?? false) && !resetTimer ? client.CurrentPresence.Timestamps : Timestamps.Now }; } private static string TrimDiscordPresenceText(string value) { if (string.IsNullOrEmpty(value) || value.Length <= MaxDiscordPresenceTextLength) return value; return value.Substring(0, MaxDiscordPresenceTextLength - 3) + "..."; } #endregion #region eventhandlers private void OnReady(object sender, ReadyMessage args) { Logger.Log($"Discord: Received Ready from user {args.User.Username}"); client?.SetPresence(CurrentPresence); } private void OnClose(object sender, CloseMessage args) { Logger.Log($"Discord: Lost Connection with client because of '{args.Reason}'"); } private void OnError(object sender, ErrorMessage args) { Logger.Log($"Discord: Error occured. ({args.Code}) {args.Message}"); } private void OnConnectionEstablished(object sender, ConnectionEstablishedMessage args) { Logger.Log($"Discord: Pipe Connection Established. Valid on pipe #{args.ConnectedPipe}"); } private void OnConnectionFailed(object sender, ConnectionFailedMessage args) { Logger.Log($"Discord: Pipe Connection Failed. Could not connect to pipe #{args.FailedPipe}"); } private void OnPresenceUpdate(object sender, PresenceMessage args) { Logger.Log($"Discord: Rich Presence Updated. State: {args.Presence?.State}; Details: {args.Presence?.Details}"); } private void OnSubscribe(object sender, SubscribeMessage args) { Logger.Log($"Discord: Subscribed: {args.Event}"); } private void OnUnsubscribe(object sender, UnsubscribeMessage args) { Logger.Log($"Discord: Unsubscribed: {args.Event}"); } #endregion public void Dispose() { if (client == null) return; if (client.IsInitialized) client.ClearPresence(); client.Dispose(); } } } ================================================ FILE: DXMainClient/Domain/FinalSunSettings.cs ================================================ using System.IO; using System.Threading.Tasks; using Rampastring.Tools; using ClientCore; using ClientCore.PlatformShim; namespace DTAClient.Domain { public static class FinalSunSettings { /// /// Checks for the existence of the FinalSun settings file and writes it if it doesn't exist. /// public static void WriteFinalSunIniAsync() { Task.Run(DoWriteFinalSunIni); } private static void DoWriteFinalSunIni() { // The encoding of the FinalSun/FinalAlert ini file should be legacy ANSI, not Windows-1252 and also not any specific encoding. // Otherwise, the map editor will not work in a non-ASCII path. ANSI doesn't mean a specific codepage, // it means the default non-Unicode codepage which can be changed from Control Panel. try { string finalSunIniPath = ClientConfiguration.Instance.FinalSunIniPath; var finalSunIniFile = new FileInfo(Path.Combine(ProgramConstants.GamePath, finalSunIniPath)); Logger.Log("Checking for the existence of FinalSun.ini."); if (finalSunIniFile.Exists) { Logger.Log("FinalSun settings file exists."); IniFile iniFile = new IniFile(); iniFile.FileName = finalSunIniFile.FullName; iniFile.Encoding = EncodingExt.ANSI; iniFile.Parse(); iniFile.SetStringValue("FinalSun", "Language", "English"); iniFile.SetStringValue("FinalSun", "FileSearchLikeTS", "yes"); iniFile.SetStringValue("TS", "Exe", SafePath.CombineDirectoryPath(ProgramConstants.GamePath)); iniFile.WriteIniFile(); return; } Logger.Log("FinalSun.ini doesn't exist - writing default settings."); if (!finalSunIniFile.Directory.Exists) finalSunIniFile.Directory.Create(); using var sw = new StreamWriter(finalSunIniFile.FullName, false, EncodingExt.ANSI); sw.WriteLine("[FinalSun]"); sw.WriteLine("Language=English"); sw.WriteLine("FileSearchLikeTS=yes"); sw.WriteLine(""); sw.WriteLine("[TS]"); sw.WriteLine("Exe=" + SafePath.CombineDirectoryPath(ProgramConstants.GamePath)); sw.WriteLine(""); sw.WriteLine("[UserInterface]"); sw.WriteLine("EasyView=0"); sw.WriteLine("NoSounds=0"); sw.WriteLine("DisableAutoLat=0"); sw.WriteLine("ShowBuildingCells=0"); } catch { Logger.Log("An exception occurred while checking the existence of FinalSun settings"); } } } } ================================================ FILE: DXMainClient/Domain/MainClientConstants.cs ================================================ using System; using System.IO; #if WINFORMS using System.Windows.Forms; #endif using ClientCore; using Rampastring.Tools; namespace DTAClient.Domain { public static class MainClientConstants { public static string GAME_NAME_LONG = "CnCNet Client"; public static string GAME_NAME_SHORT = "CnCNet"; public static string SUPPORT_URL_SHORT = "www.cncnet.org"; public static bool USE_ISOMETRIC_CELLS = true; public static int TDRA_WAYPOINT_COEFFICIENT = 128; public static int MAP_CELL_SIZE_X = 48; public static int MAP_CELL_SIZE_Y = 24; public static OSVersion OSId = OSVersion.UNKNOWN; // TODO: remove this variable after `Logger.Initialized` property is implemented by upstream public static bool LoggerInitialized { get; set; } = false; private static Action displayErrorAction = null; /// /// Gets or sets the action to perform to notify the user of an error. /// public static Action DisplayErrorAction { get => displayErrorAction ??= DefaultDisplayErrorAction; set => displayErrorAction = value; } /// /// Show an error in console as well as a Win32 MessageBox. For non-Windows platforms, this launches a text file in a GUI editor. /// This action handles errors when XNA windows are not initialized yet. /// /// The title. /// The error. /// Whether the client exits. public static void DefaultDisplayErrorAction(string title, string error, bool exit) { Console.WriteLine(title); Console.WriteLine(); Console.WriteLine(error); if (LoggerInitialized) Logger.Log(FormattableString.Invariant($"{(title is null ? null : title + Environment.NewLine + Environment.NewLine)}{error}")); #if WINFORMS MessageBox.Show(error, title, MessageBoxButtons.OK, MessageBoxIcon.Error); #else if (LoggerInitialized) ProcessLauncher.StartShellProcess(ProgramConstants.LogFileName); else { string tempfile = SafePath.CombineFilePath(Path.GetTempPath(), "xna-cncnet-client-error.log"); using (StreamWriter writer = new StreamWriter(tempfile)) { writer.WriteLine(title); writer.WriteLine(); writer.WriteLine(error); } ProcessLauncher.StartShellProcess(tempfile); } #endif if (exit) Environment.Exit(1); } public static void Initialize() { var clientConfiguration = ClientConfiguration.Instance; OSId = clientConfiguration.GetOperatingSystemVersion(); GAME_NAME_SHORT = clientConfiguration.LocalGame; GAME_NAME_LONG = clientConfiguration.LongGameName; SUPPORT_URL_SHORT = clientConfiguration.ShortSupportURL; USE_ISOMETRIC_CELLS = clientConfiguration.UseIsometricCells; TDRA_WAYPOINT_COEFFICIENT = clientConfiguration.WaypointCoefficient; MAP_CELL_SIZE_X = clientConfiguration.MapCellSizeX; MAP_CELL_SIZE_Y = clientConfiguration.MapCellSizeY; if (string.IsNullOrEmpty(GAME_NAME_SHORT)) throw new ClientConfigurationException("LocalGame is set to an empty value."); if (GAME_NAME_SHORT.Length > ProgramConstants.GAME_ID_MAX_LENGTH) { throw new ClientConfigurationException("LocalGame is set to a value that exceeds length limit of " + ProgramConstants.GAME_ID_MAX_LENGTH + " characters."); } } } } ================================================ FILE: DXMainClient/Domain/Mission.cs ================================================ using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using ClientCore; using ClientCore.Enums; using ClientCore.Extensions; using Rampastring.Tools; namespace DTAClient.Domain { /// /// A Tiberian Sun mission listed in Battle(E).ini. /// public class Mission { public Mission(IniSection missionSection, string missionCodeName) { if (missionSection == null) throw new ArgumentNullException(nameof(missionSection)); CD = missionSection.GetIntValue(nameof(CD), 0); Side = missionSection.GetIntValue(nameof(Side), 0); Scenario = missionSection.GetStringValue(nameof(Scenario), string.Empty); UntranslatedGUIName = missionSection.GetStringValue("Description", "Undefined mission"); GUIName = UntranslatedGUIName .L10N($"INI:Missions:{missionCodeName}:Description"); IconPath = missionSection.GetStringValue("SideName", string.Empty); GUIDescription = missionSection.GetStringValue("LongDescription", string.Empty) .FromIniString() .L10N($"INI:Missions:{missionCodeName}:LongDescription"); FinalMovie = missionSection.GetStringValue(nameof(FinalMovie), "none"); RequiredAddon = missionSection.GetBooleanValue(nameof(RequiredAddon), ClientConfiguration.Instance.ClientGameType == ClientType.YR || ClientConfiguration.Instance.ClientGameType == ClientType.Ares ? true : // In case of YR this toggles Ra2Mode instead which should not be default false ); Enabled = missionSection.GetBooleanValue(nameof(Enabled), true); BuildOffAlly = missionSection.GetBooleanValue(nameof(BuildOffAlly), false); PlayerAlwaysOnNormalDifficulty = missionSection.GetBooleanValue(nameof(PlayerAlwaysOnNormalDifficulty), false); Tags = missionSection.GetStringValue(nameof(Tags), string.Empty).Split(','); CodeName = missionCodeName; CustomMissionID = ComputeCustomMissionID(missionCodeName); PreviewImage = missionSection.GetStringValue("PreviewImage", string.Empty); } public static Mission NewCustomMission(IniSection clientMissionConfigSection, string missionCodeName, string scenario, IniSection? gameMissionConfigSection) { var mission = new Mission(clientMissionConfigSection, missionCodeName) { IsCustomMission = true, Scenario = scenario, GameMissionConfigSection = gameMissionConfigSection, Tags = ["CUSTOM"], }; return mission; } private static int ComputeCustomMissionID(string missionCodeName) { #pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms #pragma warning disable CA1850 // Prefer static 'HashData' method over 'ComputeHash' using var sha1 = SHA1.Create(); byte[] digest = sha1.ComputeHash(Encoding.UTF8.GetBytes(missionCodeName)); return BinaryPrimitives.ReadInt32LittleEndian(digest); #pragma warning restore CA1850 // Prefer static 'HashData' method over 'ComputeHash' #pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms } public string CodeName { get; private set; } public int CampaignID { get; } = -1; public int CustomMissionID { get; private set; } public int CD { get; private set; } public int Side { get; private set; } /// /// Refers to the map file. Must be a relative path to the game folder. /// public string Scenario { get; private set; } public string GUIName { get; private set; } public string UntranslatedGUIName { get; private set; } public string IconPath { get; private set; } public string GUIDescription { get; private set; } public string FinalMovie { get; private set; } public bool RequiredAddon { get; private set; } public bool Enabled { get; set; } public bool BuildOffAlly { get; private set; } public bool PlayerAlwaysOnNormalDifficulty { get; private set; } public IReadOnlyCollection Tags { get; private set; } /// /// This property is not set through the ini file. /// For a user custom mission, "scenario" will be assumed as the filename of a map file, with the suffix ".map" (case-insensitive). /// The map file is assumed to be placed at ClientConfiguration.CustomMissionPath. /// When launching a user custom mission, all supplemental files, i.e., files with the same filename (excepts for the suffix), will be temporarily copied into game folder. /// public bool IsCustomMission { get; private set; } public IniSection? GameMissionConfigSection { get; set; } public string PreviewImage { get; private set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/AllianceHolder.cs ================================================ using ClientCore; using ClientCore.Enums; using Rampastring.Tools; using System.Collections.Generic; namespace DTAClient.Domain.Multiplayer { /// /// A helper class for setting up alliances in spawn.ini. /// public static class AllianceHolder { public static void WriteInfoToSpawnIni( List players, List aiPlayers, List multiCmbIndexes, List playerHouseInfos, List teamStartMappings, IniFile spawnIni ) { List team1MultiMemberIds = new List(); List team2MultiMemberIds = new List(); List team3MultiMemberIds = new List(); List team4MultiMemberIds = new List(); for (int pId = 0; pId < players.Count; pId++) { var phi = playerHouseInfos[pId]; int teamId = players[pId].TeamId; if (teamId <= 0) teamId = teamStartMappings?.Find(sa => sa.StartingWaypoint == phi.StartingWaypoint)?.TeamId ?? 0; if (teamId > 0) { switch (teamId) { case 1: team1MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1); break; case 2: team2MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1); break; case 3: team3MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1); break; case 4: team4MultiMemberIds.Add(multiCmbIndexes.FindIndex(c => c == pId) + 1); break; } } } int multiId = multiCmbIndexes.Count + 1; for (int aiId = 0; aiId < aiPlayers.Count; aiId++) { var phi = playerHouseInfos[multiCmbIndexes.Count + aiId]; int teamId = aiPlayers[aiId].TeamId; if (teamId <= 0) teamId = teamStartMappings?.Find(sa => sa.StartingWaypoint == phi.StartingWaypoint)?.TeamId ?? 0; if (teamId > 0) { switch (teamId) { case 1: team1MultiMemberIds.Add(multiId); break; case 2: team2MultiMemberIds.Add(multiId); break; case 3: team3MultiMemberIds.Add(multiId); break; case 4: team4MultiMemberIds.Add(multiId); break; } } multiId++; } WriteAlliances(team1MultiMemberIds, spawnIni); WriteAlliances(team2MultiMemberIds, spawnIni); WriteAlliances(team3MultiMemberIds, spawnIni); WriteAlliances(team4MultiMemberIds, spawnIni); } private static void WriteAlliances(List teamHouseMemberIds, IniFile spawnIni) { foreach (int houseId in teamHouseMemberIds) { bool selfFound = false; for (int allyId = 0; allyId < teamHouseMemberIds.Count; allyId++) { int allyHouseId = teamHouseMemberIds[allyId]; if (allyHouseId == houseId) selfFound = true; else { spawnIni.SetIntValue("Multi" + houseId + "_Alliances", "HouseAlly" + GetHouseAllyIndexString(allyId, selfFound), ClientConfiguration.Instance.ClientGameType == ClientType.RA ? allyHouseId + 11 // Compared with other games, Red Alert uses house IDs shifted by +12 (from -1 to +11) in multiplayer : allyHouseId - 1); } } } } private static string GetHouseAllyIndexString(int allyId, bool selfFound) { if (selfFound) allyId = allyId - 1; switch (allyId) { case 0: return "One"; case 1: return "Two"; case 2: return "Three"; case 3: return "Four"; case 4: return "Five"; case 5: return "Six"; case 6: return "Seven"; } return "None" + allyId; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CacheManagerBase.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Threading; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer; /// /// Thread-safe manager for caching outputs with LRU eviction policy. /// Processes computation requests sequentially to limit CPU usage to a single thread. /// Note: this manager assumes the `TOutput` objects are managed, so it never disposes them directly. /// public abstract class CacheManagerBase : ICacheManager where TInput : notnull { public abstract string Name { get; } private const int WorkerThreadShutdownTimeoutMs = 2000; private readonly int capacity; private readonly object cacheLock = new(); private readonly Dictionary cache = new(); private readonly LinkedList lruList = new(); private readonly HashSet requestQueue = new(); private readonly object queueLock = new(); private readonly Thread? workerThread; private volatile bool isDisposed = false; public int Count => cache.Count; /// /// Represents a cached TOutput entry with its position in the LRU list. /// private class CacheEntry { public TOutput? Output { get; } public LinkedListNode LruNode { get; set; } public CacheEntry(TOutput? output, LinkedListNode lruNode) { Output = output; LruNode = lruNode; } } /// /// Initializes a new instance of the InputPreviewCacheManager and start the worker thread immediately. /// /// Maximum number of outputs to keep in cache. Must be positive. public CacheManagerBase(int capacity) { if (capacity <= 0) throw new ArgumentException("Capacity must be positive.", nameof(capacity)); this.capacity = capacity; workerThread = new Thread(ProcessRequests) { IsBackground = true, Name = $"{Name}-Worker" }; workerThread.Start(); } /// /// Attempts to get a cached output for the specified input. /// Updates LRU order if found. /// /// The input. /// The cached output if found. /// True if the output was found in cache; otherwise false. private bool TryGet(TInput input, out TOutput? output) { if (input == null) throw new ArgumentNullException(nameof(input)); lock (cacheLock) { if (cache.TryGetValue(input, out CacheEntry? entry)) { // Move to front of LRU list (most recently used) lruList.Remove(entry.LruNode); entry.LruNode = lruList.AddFirst(input); output = entry.Output; return true; } output = default; return false; } } public bool Request(TInput input, out TOutput? output, bool syncLoadOnCacheMiss = false, bool addToQueue = true) { if (input == null) throw new ArgumentNullException(nameof(input)); if (isDisposed) throw new ObjectDisposedException(nameof(CacheManagerBase)); // Check if already cached if (TryGet(input, out TOutput? cachedOutput)) { output = cachedOutput; return true; } // If not cached and sync load is allowed, attempt to load immediately (may be CPU-intensive) if (syncLoadOnCacheMiss) { output = ComputeOutputForInput(input); // Add to cache even if the output is null AddToCache(input, output); return true; } // Queue for processing (HashSet prevents duplicates) if (addToQueue) { lock (queueLock) { if (requestQueue.Add(input)) { // Signal worker thread that new work is available Monitor.Pulse(queueLock); } } } output = default; return false; } /// /// Manually adds an output to the cache. /// Useful for pre-loading or when output is obtained from other sources. /// Note: If the input is already cached, this method updates LRU order but does NOT replace the cached output. /// /// The input. /// The output. /// True if the output was added to cache; false if output was already cached. private bool AddToCache(TInput input, TOutput? output) { if (input == null) throw new ArgumentNullException(nameof(input)); lock (cacheLock) { // If already cached, update LRU order but don't replace if (cache.TryGetValue(input, out CacheEntry? existingEntry)) { lruList.Remove(existingEntry.LruNode); existingEntry.LruNode = lruList.AddFirst(input); return false; } // Evict if at capacity if (cache.Count >= capacity) EvictLeastRecentlyUsed(); // Add new entry LinkedListNode node = lruList.AddFirst(input); cache[input] = new CacheEntry(output, node); return true; } } public void Clear() { lock (cacheLock) { cache.Clear(); lruList.Clear(); } } /// /// Computes the output for a given input. This method may or might not be called by the worker thread and may be CPU-intensive. /// /// The input. /// The output. protected abstract TOutput? ComputeOutputForInput(TInput input); /// /// Worker thread that processes computation requests sequentially. /// private void ProcessRequests() { while (!isDisposed) { TInput? input = default; bool inputFound = false; lock (queueLock) { // Wait for work or disposal while (requestQueue.Count == 0 && !isDisposed) { Monitor.Wait(queueLock); } // Exit if disposed if (isDisposed) break; // Get first item from HashSet using var enumerator = requestQueue.GetEnumerator(); if (enumerator.MoveNext()) { inputFound = true; input = enumerator.Current; requestQueue.Remove(input); } } // If no input, loop back to wait if (!inputFound) continue; try { // Check if already cached (might have been computed by another request) if (TryGet(input!, out _)) continue; // Get the output for the input. This is the CPU-intensive operation. TOutput? output = ComputeOutputForInput(input!); // Add to cache even if the output is null AddToCache(input!, output); } catch (Exception ex) { Logger.Log($"{Name}: Failed to get the output for input '{input}'. Error: {ex.ToString()}"); } } } /// /// Evicts the least recently used output from the cache. /// Must be called within cacheLock. /// private void EvictLeastRecentlyUsed() { if (lruList.Last == null) return; TInput lruInput = lruList.Last.Value; lruList.RemoveLast(); if (cache.TryGetValue(lruInput, out CacheEntry? entry)) cache.Remove(lruInput); } /// /// Disposes the cache manager. Does not dispose cached outputs directly; left to garbage collector. /// public void Dispose() { if (isDisposed) return; isDisposed = true; // Signal worker thread to stop lock (queueLock) { Monitor.Pulse(queueLock); } // Wait for worker thread to finish if (workerThread != null && workerThread.IsAlive) { if (!workerThread.Join(WorkerThreadShutdownTimeoutMs)) { // Log warning if thread doesn't terminate gracefully Logger.Log($"{Name}: Worker thread did not terminate within timeout period."); } } // Clear cache Clear(); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/CnCNetGame.cs ================================================ #nullable enable using System; using System.Threading; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using SixLabors.ImageSharp; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A class for games supported on CnCNet (DTA, TI, TS, RA1/2, etc.) /// public abstract class CnCNetGame { private readonly Lazy lazyImage; private readonly Lazy lazyTexture; protected CnCNetGame() { lazyImage = new Lazy(LoadImage, LazyThreadSafetyMode.ExecutionAndPublication); lazyTexture = new Lazy(LoadTexture, LazyThreadSafetyMode.None); } /// /// The name of the game that is displayed on the user-interface. /// public string? UIName { get; set; } /// /// The internal name (suffix) of the game. /// public string? InternalName { get; set; } /// /// The IRC chat channel ID of the game. /// public string? ChatChannel { get; set; } /// /// The IRC game broadcasting channel ID of the game. /// public string? GameBroadcastChannel { get; set; } /// /// The executable name of the game's client. /// public string? ClientExecutableName { get; set; } /// /// Gets the image for this game's icon. Loaded lazily and is thread-safe. /// public Image? Image => lazyImage.Value; /// /// Gets the texture for this game's icon. Loaded lazily; must be accessed only from the main (graphics) thread. /// public Texture2D? Texture => lazyTexture.Value; /// /// The location where to read the game's installation path from the registry. /// public string? RegistryInstallPath { get => field; set { string? hive = value?.Split('\\')[0].Trim(); if (hive is not "HKLM" and not "HKCU") throw new Exception($"Unexpected registry hive. Expected HKLM or HKCU. Got: {hive}"); field = value; } } private bool supported = true; /// /// Determines if the game is properly supported by this client. /// Defaults to true. /// public bool Supported { get { return supported; } set { supported = value; } } /// /// If true, the client should always be connected to this game's chat channel. /// public bool AlwaysEnabled { get; set; } /// /// Loads the image for this game's icon. Thread-safe. /// protected abstract Image? LoadImage(); /// /// Loads the texture for this game's icon. Must be called from the main (graphics) thread. /// Note: the instance is kept alive for the lifetime of this object, /// since the lazy value cannot be explicitly disposed after texture creation. For the small /// icon images used here this is an acceptable trade-off. /// protected virtual Texture2D? LoadTexture() { Image? image = Image; if (image == null) return null; return AssetLoader.TextureFromImage(image); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs ================================================ #nullable enable using ClientCore; using System; using System.Threading; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A class for updating of the CnCNet game/player count. /// public static class CnCNetPlayerCountTask { public static int PlayerCount { get; private set; } private static int REFRESH_INTERVAL = 60000; // 1 minute internal static event EventHandler? CnCNetGameCountUpdated; private static string? cncnetLiveStatusIdentifier; public static void InitializeService(CancellationTokenSource cts) { cncnetLiveStatusIdentifier = ClientConfiguration.Instance.CnCNetLiveStatusIdentifier; // This call is synchronous. Therefore, we use a short timeout to avoid blocking the main thread for too long. PlayerCount = GetCnCNetPlayerCount(timeoutMilliseconds: 1000); CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(PlayerCount)); ThreadPool.QueueUserWorkItem(new WaitCallback(RunService), cts); } private static void RunService(object? tokenObj) { var waitHandle = ((CancellationTokenSource)tokenObj!).Token.WaitHandle; while (true) { if (waitHandle.WaitOne(REFRESH_INTERVAL)) { // Cancellation signaled return; } else { CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(GetCnCNetPlayerCount(timeoutMilliseconds: 5000))); } } } private static int GetCnCNetPlayerCount(int timeoutMilliseconds = 5000) { try { // Don't fetch the player count if it is explicitly disabled. // For example, the official CnCNet server might be unavailable/unstable in a country with Internet censorship, // which causes lags in the splash screen. In the worst case, say if packets are dropped, it waits until timeouts. if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetPlayerCountURL)) return -1; string info = new TimedHttpClient(timeoutMilliseconds) .GetString(ClientConfiguration.Instance.CnCNetPlayerCountURL); info = info.Replace("{", string.Empty); info = info.Replace("}", string.Empty); info = info.Replace("\"", string.Empty); string[] values = info.Split(new char[] { ',' }); int numGames = -1; foreach (string value in values) { if (value.Contains(cncnetLiveStatusIdentifier!)) { numGames = Convert.ToInt32(value.Substring(cncnetLiveStatusIdentifier!.Length + 1)); return numGames; } } return numGames; } catch { return -1; } } } internal class PlayerCountEventArgs : EventArgs { public PlayerCountEventArgs(int playerCount) { PlayerCount = playerCount; } public int PlayerCount { get; set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs ================================================ using Rampastring.Tools; using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Net.NetworkInformation; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A CnCNet tunnel server. /// public class CnCNetTunnel { private const int REQUEST_TIMEOUT = 10000; // In milliseconds private const int PING_TIMEOUT = 1000; public CnCNetTunnel() { } /// /// Parses a formatted string that contains the tunnel server's /// information into a CnCNetTunnel instance. /// /// The string that contains the tunnel server's information. /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str) { // For the format, check http://cncnet.org/master-list try { var tunnel = new CnCNetTunnel(); string[] parts = str.Split(';'); string address = parts[0]; string[] detailedAddress = address.Split(new char[] { ':' }); tunnel.Address = detailedAddress[0]; tunnel.Port = int.Parse(detailedAddress[1]); tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; tunnel.Name = parts[3]; tunnel.RequiresPassword = parts[4] != "0"; tunnel.Clients = int.Parse(parts[5]); tunnel.MaxClients = int.Parse(parts[6]); int status = int.Parse(parts[7]); tunnel.Official = status == 2; if (!tunnel.Official) tunnel.Recommended = status == 1; CultureInfo cultureInfo = CultureInfo.InvariantCulture; tunnel.Latitude = double.Parse(parts[8], cultureInfo); tunnel.Longitude = double.Parse(parts[9], cultureInfo); tunnel.Version = int.Parse(parts[10]); tunnel.Distance = double.Parse(parts[11], cultureInfo); return tunnel; } catch (Exception ex) { if (ex is FormatException || ex is OverflowException || ex is IndexOutOfRangeException) { Logger.Log("Parsing tunnel information failed: " + ex.ToString() + Environment.NewLine + "Parsed string: " + str); return null; } throw; } } public string Address { get; private set; } public int Port { get; private set; } public string Country { get; private set; } public string CountryCode { get; private set; } public string Name { get; private set; } public bool RequiresPassword { get; private set; } public int Clients { get; private set; } public int MaxClients { get; private set; } public bool Official { get; private set; } public bool Recommended { get; private set; } public double Latitude { get; private set; } public double Longitude { get; private set; } public int Version { get; private set; } public double Distance { get; private set; } public int PingInMs { get; set; } = -1; /// /// Updates this tunnel's metadata from another tunnel instance, preserving Address, Port, and existing PingInMs. /// internal void UpdateFrom(CnCNetTunnel updatedTunnel) { Country = updatedTunnel.Country; CountryCode = updatedTunnel.CountryCode; Name = updatedTunnel.Name; Clients = updatedTunnel.Clients; MaxClients = updatedTunnel.MaxClients; Official = updatedTunnel.Official; Recommended = updatedTunnel.Recommended; Version = updatedTunnel.Version; RequiresPassword = updatedTunnel.RequiresPassword; Latitude = updatedTunnel.Latitude; Longitude = updatedTunnel.Longitude; Distance = updatedTunnel.Distance; } /// /// Gets a list of player ports to use from a specific V2 tunnel server. /// /// A list of player ports to use. public List GetPlayerPortInfo(int playerCount) { try { Logger.Log($"Contacting tunnel at {Address}:{Port}"); // Do not use https here as not supported by tunnels string addressString = $"http://{Address}:{Port}/request?clients={playerCount}"; Logger.Log($"Downloading from {addressString}"); string data = new TimedHttpClient(REQUEST_TIMEOUT).GetString(addressString); data = data.Replace("[", String.Empty); data = data.Replace("]", String.Empty); string[] portIDs = data.Split(','); List playerPorts = new List(); foreach (string _port in portIDs) { playerPorts.Add(Convert.ToInt32(_port)); Logger.Log($"Added port {_port}"); } return playerPorts; } catch (Exception ex) { Logger.Log("Unable to connect to the specified tunnel server. Returned error message: " + ex.ToString()); } return new List(); } public void UpdatePing() { using (Ping p = new Ping()) { try { PingReply reply = p.Send(IPAddress.Parse(Address), PING_TIMEOUT); if (reply.Status == IPStatus.Success) PingInMs = Convert.ToInt32(reply.RoundtripTime); } catch (PingException ex) { Logger.Log($"Caught an exception when pinging {Name} tunnel server: {ex.ToString()}"); } } } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/CustomCnCNetGame.cs ================================================ #nullable enable using System; using System.Reflection; using Microsoft.Xna.Framework.Graphics; using Rampastring.XNAUI; using SixLabors.ImageSharp; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A that loads its texture from a custom icon file, or falls back /// to the unknown game icon embedded in the assembly. /// internal sealed class CustomCnCNetGame : CnCNetGame { private static readonly Lazy lazyFallbackImage = new(() => Image.Load( Assembly.GetAssembly(typeof(CustomCnCNetGame))! .GetManifestResourceStream("DTAClient.Icons.unknownicon.png"))); private static Image FallbackImage => lazyFallbackImage.Value; private readonly string iconFilename; public CustomCnCNetGame(string iconFilename) { this.iconFilename = iconFilename; } protected override Image? LoadImage() => FallbackImage; protected override Texture2D? LoadTexture() { if (AssetLoader.AssetExists(iconFilename)) return AssetLoader.LoadTexture(iconFilename); return base.LoadTexture(); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/DefaultCnCNetGame.cs ================================================ #nullable enable using System.Reflection; using SixLabors.ImageSharp; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A that loads its icon from an embedded assembly resource. /// internal sealed class DefaultCnCNetGame : CnCNetGame { private static readonly Assembly assembly = Assembly.GetAssembly(typeof(DefaultCnCNetGame))!; private readonly string iconResourceName; public DefaultCnCNetGame(string iconResourceName) { this.iconResourceName = iconResourceName; } protected override Image? LoadImage() { using var stream = assembly.GetManifestResourceStream(iconResourceName); if (stream == null) return null; return Image.Load(stream); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/GameCollection.cs ================================================ using System.Collections.Generic; using System.Linq; using System; using System.Threading.Tasks; using Rampastring.Tools; using ClientCore; using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A class for storing the collection of supported CnCNet games. /// public class GameCollection { public List GameList { get; private set; } public GameCollection() { Initialize(); } public void Initialize() { GameList = new List(); // Default supported games. Images are loaded lazily in background threads; // textures are created on demand on the main thread when first accessed. var defaultGames = new DefaultCnCNetGame[] { new DefaultCnCNetGame("DTAClient.Icons.dtaicon.png") { ChatChannel = "#cncnet-dta", ClientExecutableName = "DTA.exe", GameBroadcastChannel = "#cncnet-dta-games", InternalName = "dta", RegistryInstallPath = "HKCU\\Software\\TheDawnOfTheTiberiumAge", UIName = "Dawn of the Tiberium Age".L10N("Client:ClientCore:DawnoftheTiberiumAge") }, new DefaultCnCNetGame("DTAClient.Icons.tiicon.png") { ChatChannel = "#cncnet-ti", ClientExecutableName = "TI_Launcher.exe", GameBroadcastChannel = "#cncnet-ti-games", InternalName = "ti", RegistryInstallPath = "HKCU\\Software\\TwistedInsurrection", UIName = "Twisted Insurrection".L10N("Client:ClientCore:TwistedInsurrection") }, new DefaultCnCNetGame("DTAClient.Icons.moicon.png") { ChatChannel = "#cncnet-mo", ClientExecutableName = "MentalOmegaClient.exe", GameBroadcastChannel = "#cncnet-mo-games", InternalName = "mo", RegistryInstallPath = "HKCU\\Software\\MentalOmega", UIName = "Mental Omega".L10N("Client:ClientCore:MentalOmega") }, new DefaultCnCNetGame("DTAClient.Icons.rricon.png") { ChatChannel = "#redres-lobby", ClientExecutableName = "RRLauncher.exe", GameBroadcastChannel = "#redres-games", InternalName = "rr", RegistryInstallPath = "HKLM\\Software\\RedResurrection", UIName = "YR Red-Resurrection".L10N("Client:ClientCore:YRRedResurrection") }, new DefaultCnCNetGame("DTAClient.Icons.reicon.png") { ChatChannel = "#riseoftheeast", ClientExecutableName = "RELauncher.exe", GameBroadcastChannel = "#rote-games", InternalName = "re", RegistryInstallPath = "HKLM\\Software\\RiseoftheEast", UIName = "Rise of the East".L10N("Client:ClientCore:RiseoftheEast") }, new DefaultCnCNetGame("DTAClient.Icons.cncricon.png") { ChatChannel = "#cncreloaded", ClientExecutableName = "CnCReloadedClient.exe", GameBroadcastChannel = "#cncreloaded-games", InternalName = "cncr", RegistryInstallPath = "HKCU\\Software\\CnCReloaded", UIName = "C&C: Reloaded".L10N("Client:ClientCore:CnCReloaded") }, new DefaultCnCNetGame("DTAClient.Icons.tdicon.png") { ChatChannel = "#cncnet-td", ClientExecutableName = "TiberianDawn.exe", GameBroadcastChannel = "#cncnet-td-games", InternalName = "td", RegistryInstallPath = "HKLM\\Software\\Westwood\\Tiberian Dawn", UIName = "Tiberian Dawn".L10N("Client:ClientCore:TiberianDawn"), Supported = false }, new DefaultCnCNetGame("DTAClient.Icons.raicon.png") { ChatChannel = "#cncnet-ra", ClientExecutableName = "RedAlert.exe", GameBroadcastChannel = "#cncnet-ra-games", InternalName = "ra", RegistryInstallPath = "HKLM\\Software\\Westwood\\Red Alert", UIName = "Red Alert".L10N("Client:ClientCore:RedAlert") }, new DefaultCnCNetGame("DTAClient.Icons.d2kicon.png") { ChatChannel = "#cncnet-d2k", ClientExecutableName = "Dune2000.exe", GameBroadcastChannel = "#cncnet-d2k-games", InternalName = "d2k", RegistryInstallPath = "HKLM\\Software\\Westwood\\Dune 2000", UIName = "Dune 2000".L10N("Client:ClientCore:Dune2000"), Supported = false }, new DefaultCnCNetGame("DTAClient.Icons.tsicon.png") { ChatChannel = "#cncnet-ts", ClientExecutableName = "TiberianSun.exe", GameBroadcastChannel = "#cncnet-ts-games", InternalName = "ts", RegistryInstallPath = "HKLM\\Software\\Westwood\\Tiberian Sun", UIName = "Tiberian Sun".L10N("Client:ClientCore:TiberianSun") }, new DefaultCnCNetGame("DTAClient.Icons.yricon.png") { ChatChannel = "#cncnet-yr", ClientExecutableName = "CnCNetClientYR.exe", GameBroadcastChannel = "#cncnet-yr-games", InternalName = "yr", RegistryInstallPath = "HKLM\\Software\\Westwood\\Yuri's Revenge", UIName = "Yuri's Revenge".L10N("Client:ClientCore:YurisRevenge") }, new DefaultCnCNetGame("DTAClient.Icons.ssicon.png") { ChatChannel = "#cncnet-ss", ClientExecutableName = "SoleSurvivor.exe", GameBroadcastChannel = "#cncnet-ss-games", InternalName = "ss", RegistryInstallPath = "HKLM\\Software\\Westwood\\Sole Survivor", UIName = "Sole Survivor".L10N("Client:ClientCore:SoleSurvivor"), Supported = false } }; // CnCNet chat. var otherGames = new DefaultCnCNetGame[] { new DefaultCnCNetGame("DTAClient.Icons.cncneticon.png") { ChatChannel = "#cncnet", InternalName = "cncnet", UIName = "General CnCNet Chat".L10N("Client:ClientCore:GeneralCnCNetChat"), AlwaysEnabled = true } }; GameList.AddRange(defaultGames); GameList.AddRange(GetCustomGames(defaultGames.Concat(otherGames).ToList())); GameList.AddRange(otherGames); if (GetGameIndexFromInternalName(ClientConfiguration.Instance.LocalGame) == -1) { throw new ClientConfigurationException("Could not find a game in the game collection matching LocalGame value of " + ClientConfiguration.Instance.LocalGame + "."); } // Fire-and-forget background preloading of images. var gamesToPreload = GameList.ToList(); _ = Task.Run(() => { foreach (var game in gamesToPreload) _ = game.Image; }); } private List GetCustomGames(List existingGames) { IniFile iniFile = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "GameCollectionConfig.ini")); List customGames = new List(); var section = iniFile.GetSection("CustomGames"); if (section == null) return customGames; HashSet customGameIDs = new HashSet(); foreach (var kvp in section.Keys) { if (!iniFile.SectionExists(kvp.Value)) continue; string ID = iniFile.GetStringValue(kvp.Value, "InternalName", string.Empty).ToLowerInvariant(); if (string.IsNullOrEmpty(ID)) throw new GameCollectionConfigurationException("InternalName for game " + kvp.Value + " is not defined or set to an empty value."); if (ID.Length > ProgramConstants.GAME_ID_MAX_LENGTH) { throw new GameCollectionConfigurationException("InternalGame for game " + kvp.Value + " is set to a value that exceeds length limit of " + ProgramConstants.GAME_ID_MAX_LENGTH + " characters."); } if (existingGames.Find(g => g.InternalName == ID) != null || customGameIDs.Contains(ID)) throw new GameCollectionConfigurationException("Game with InternalName " + ID.ToUpperInvariant() + " already exists in the game collection."); string iconFilename = iniFile.GetStringValue(kvp.Value, "IconFilename", ID + "icon.png"); CustomCnCNetGame customCnCNetGame; try { customCnCNetGame = new CustomCnCNetGame(iconFilename) { InternalName = ID, UIName = iniFile.GetStringValue(kvp.Value, "UIName", ID.ToUpperInvariant()), ChatChannel = GetIRCChannelNameFromIniFile(iniFile, kvp.Value, "ChatChannel"), GameBroadcastChannel = GetIRCChannelNameFromIniFile(iniFile, kvp.Value, "GameBroadcastChannel"), ClientExecutableName = iniFile.GetStringValue(kvp.Value, "ClientExecutableName", string.Empty), RegistryInstallPath = iniFile.GetStringValue(kvp.Value, "RegistryInstallPath", "HKCU\\Software\\" + ID.ToUpperInvariant()) }; } catch (Exception ex) { throw new GameCollectionConfigurationException("Error while reading GameCollectionConfig.ini for game " + kvp.Value + ": " + ex.Message); } customGames.Add(customCnCNetGame); customGameIDs.Add(ID); } return customGames; } private string GetIRCChannelNameFromIniFile(IniFile iniFile, string section, string key) { string channel = iniFile.GetStringValue(section, key, string.Empty); if (string.IsNullOrEmpty(channel)) throw new GameCollectionConfigurationException(key + " for game " + section + " is not defined or set to an empty value."); if (channel.Contains(' ') || channel.Contains(',') || channel.Contains((char)7)) throw new GameCollectionConfigurationException(key + " for game " + section + " contains characters not allowed on IRC channel names."); if (!channel.StartsWith("#")) return "#" + channel; return channel; } /// /// Gets the index of a CnCNet supported game based on its internal name. /// /// The internal name (suffix) of the game. /// The index of the specified CnCNet game. -1 if the game is unknown or not supported. public int GetGameIndexFromInternalName(string gameName) { for (int gId = 0; gId < GameList.Count; gId++) { CnCNetGame game = GameList[gId]; if (gameName.ToLowerInvariant() == game.InternalName) return gId; } return -1; } /// /// Seeks the supported game list for a specific game's internal name and if found, /// returns the game's full name. Otherwise returns the internal name specified in the param. /// /// The internal name of the game to seek for. /// The full name of a supported game based on its internal name. /// Returns the given parameter if the name isn't found in the supported game list. public string GetGameNameFromInternalName(string gameName) { CnCNetGame game = GameList.Find(g => g.InternalName == gameName.ToLowerInvariant()); if (game == null) return gameName; return game.UIName; } /// /// Returns the full UI name of a game based on its index in the game list. /// /// The index of the CnCNet supported game. /// The UI name of the game. public string GetFullGameNameFromIndex(int gameIndex) { return GameList[gameIndex].UIName; } /// /// Returns the internal name of a game based on its index in the game list. /// /// The index of the CnCNet supported game. /// The internal name (suffix) of the game. public string GetGameIdentifierFromIndex(int gameIndex) { return GameList[gameIndex].InternalName; } public string GetGameBroadcastingChannelNameFromIdentifier(string gameIdentifier) { CnCNetGame game = GameList.Find(g => g.InternalName == gameIdentifier.ToLowerInvariant()); if (game == null) return null; return game.GameBroadcastChannel; } public string GetGameChatChannelNameFromIdentifier(string gameIdentifier) { CnCNetGame game = GameList.Find(g => g.InternalName == gameIdentifier.ToLowerInvariant()); if (game == null) return null; return game.ChatChannel; } } /// /// An exception that is thrown when configuration for a game to add to game collection /// contains invalid or unexpected settings / data or required settings / data are missing. /// class GameCollectionConfigurationException : Exception { public GameCollectionConfigurationException(string message) : base(message) { } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.CnCNet { public class HostedCnCNetGame : GenericHostedGame { public HostedCnCNetGame() { } public HostedCnCNetGame(string channelName, string revision, string gamever, int maxPlayers, string roomName, bool passworded, bool tunneled, string[] players, string adminName, string mapName, string gameMode, string mapHash) { ChannelName = channelName; Revision = revision; GameVersion = gamever; MaxPlayers = maxPlayers; RoomName = roomName; Passworded = passworded; Tunneled = tunneled; Players = players; HostName = adminName; Map = mapName; GameMode = gameMode; MapHash = mapHash; } public string ChannelName { get; set; } public string Revision { get; set; } public bool Tunneled { get; set; } public bool IsLadder { get; set; } public string MatchID { get; set; } public CnCNetTunnel TunnelServer { get; set; } public int[] BroadcastedGameOptionValues { get; set; } public override int Ping => TunnelServer.PingInMs; public override bool Equals(GenericHostedGame other) => other is HostedCnCNetGame hostedCnCNetGame ? string.Equals(hostedCnCNetGame.ChannelName, ChannelName, StringComparison.InvariantCultureIgnoreCase) : base.Equals(other); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/MapEventArgs.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.CnCNet { public class MapEventArgs : EventArgs { public MapEventArgs(Map map) { Map = map; } public Map Map { get; private set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Globalization; using System.Net.Http; using System.Threading; using Rampastring.Tools; using ClientCore; using System.IO.Compression; using System.Linq; using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// Handles sharing maps. /// public static class MapSharer { public static event EventHandler MapUploadFailed; public static event EventHandler MapUploadComplete; public static event EventHandler MapUploadStarted; public static event EventHandler MapDownloadFailed; public static event EventHandler MapDownloadComplete; public static event EventHandler MapDownloadStarted; private volatile static List MapDownloadQueue = new List(); private volatile static List MapUploadQueue = new List(); private volatile static List UploadedMaps = new List(); private static readonly object locker = new object(); private const int DOWNLOAD_TIMEOUT = 100000; // In milliseconds private const int UPLOAD_TIMEOUT = 100000; // In milliseconds /// /// Adds a map into the CnCNet map upload queue. /// /// The map. /// The short name of the game that is being played (DTA, TI, MO, etc). public static void UploadMap(Map map, string myGame) { lock (locker) { if (UploadedMaps.Contains(map.SHA1) || MapUploadQueue.Contains(map)) { Logger.Log("MapSharer: Already uploading map " + map.BaseFilePath + " - returning."); return; } MapUploadQueue.Add(map); if (MapUploadQueue.Count == 1) { ParameterizedThreadStart pts = new ParameterizedThreadStart(Upload); Thread thread = new Thread(pts); object[] mapAndGame = new object[2]; mapAndGame[0] = map; mapAndGame[1] = myGame.ToLower(); thread.Start(mapAndGame); } } } private static void Upload(object mapAndGame) { object[] mapGameArray = (object[])mapAndGame; Map map = (Map)mapGameArray[0]; string myGameId = (string)mapGameArray[1]; MapUploadStarted?.Invoke(null, new MapEventArgs(map)); Logger.Log("MapSharer: Starting upload of " + map.BaseFilePath); if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetMapDBUploadURL)) { Logger.Log("MapSharer: Upload URL is not configured."); MapUploadFailed?.Invoke(null, new MapEventArgs(map)); return; } string message = MapUpload(ClientConfiguration.Instance.CnCNetMapDBUploadURL, map, myGameId, out bool success); if (success) { MapUploadComplete?.Invoke(null, new MapEventArgs(map)); lock (locker) { UploadedMaps.Add(map.SHA1); } Logger.Log("MapSharer: Uploading map " + map.BaseFilePath + " completed succesfully."); } else { MapUploadFailed?.Invoke(null, new MapEventArgs(map)); Logger.Log("MapSharer: Uploading map " + map.BaseFilePath + " failed! Returned message: " + message); } lock (locker) { MapUploadQueue.Remove(map); if (MapUploadQueue.Count > 0) { Map nextMap = MapUploadQueue[0]; object[] array = new object[2]; array[0] = nextMap; array[1] = myGameId; Logger.Log("MapSharer: There are additional maps in the queue."); Upload(array); } } } private static string MapUpload(string _URL, Map map, string gameName, out bool success) { FileInfo zipFile = SafePath.GetFile(ProgramConstants.GamePath, "Maps", "Custom", FormattableString.Invariant($"{map.SHA1}.zip")); if (zipFile.Exists) zipFile.Delete(); string mapFileName = $"{map.SHA1}.{ClientConfiguration.Instance.MapFileExtension}"; File.Copy(SafePath.CombineFilePath(map.CompleteFilePath), SafePath.CombineFilePath(ProgramConstants.GamePath, mapFileName)); CreateZipFile(mapFileName, zipFile.FullName); try { SafePath.DeleteFileIfExists(ProgramConstants.GamePath, mapFileName); } catch { } // Upload the file to the URI. // The 'UploadFile(uriString,fileName)' method implicitly uses HTTP POST method. try { using (FileStream stream = zipFile.Open(FileMode.Open)) { List files = new List(); //{ // new FileToUpload // { // Name = "file", // Filename = Path.GetFileName(zipFile), // ContentType = "mapZip", // Stream = stream // }; //}; FileToUpload file = new FileToUpload() { Name = "file", Filename = zipFile.Name, ContentType = "mapZip", Stream = stream }; files.Add(file); byte[] responseArray = UploadFiles(_URL, files, gameName.ToLower()); string response = Encoding.UTF8.GetString(responseArray); if (!response.Contains("Upload succeeded!")) { success = false; return response; } Logger.Log("MapSharer: Upload response: " + response); //MessageBox.Show((response)); success = true; return String.Empty; } } catch (Exception ex) { success = false; return ex.Message; } } private static byte[] UploadFiles(string address, List files, string gameName) { using var content = new MultipartFormDataContent(); content.Add(new StringContent(gameName), "game"); foreach (FileToUpload file in files) { var streamContent = new StreamContent(file.Stream); streamContent.Headers.TryAddWithoutValidation("Content-Type", file.ContentType); content.Add(streamContent, file.Name, file.Filename); } return new TimedHttpClient(UPLOAD_TIMEOUT).Post(address, content); } private static void CreateZipFile(string file, string zipName) { using var zipFileStream = new FileStream(zipName, FileMode.CreateNew, FileAccess.Write); using var archive = new ZipArchive(zipFileStream, ZipArchiveMode.Create); archive.CreateEntryFromFile(SafePath.CombineFilePath(ProgramConstants.GamePath, file), file); } private static string ExtractZipFile(string zipFile, string destDir) { using ZipArchive zipArchive = ZipFile.OpenRead(zipFile); // here, we extract every entry, but we could extract conditionally // based on entry name, size, date, checkbox status, etc. zipArchive.ExtractToDirectory(destDir); return zipArchive.Entries.FirstOrDefault()?.Name; } public static void DownloadMap(string sha1, string myGame, string mapName) { lock (locker) { if (MapDownloadQueue.Contains(sha1)) { Logger.Log("MapSharer: Map " + sha1 + " already exists in the download queue."); return; } MapDownloadQueue.Add(sha1); if (MapDownloadQueue.Count == 1) { object[] details = new object[3]; details[0] = sha1; details[1] = myGame.ToLower(); details[2] = mapName; ParameterizedThreadStart pts = new ParameterizedThreadStart(Download); Thread thread = new Thread(pts); thread.Start(details); } } } private static void Download(object details) { object[] sha1AndGame = (object[])details; string sha1 = (string)sha1AndGame[0]; string myGameId = (string)sha1AndGame[1]; string mapName = (string)sha1AndGame[2]; Logger.Log("MapSharer: Preparing to download map " + sha1 + " with name: " + mapName); bool success; try { Logger.Log("MapSharer: MapDownloadStarted"); MapDownloadStarted?.Invoke(null, new SHA1EventArgs(sha1, mapName)); } catch (Exception ex) { Logger.Log("MapSharer: ERROR " + ex.ToString()); } string mapPath = DownloadMain(sha1, myGameId, mapName, out success); lock (locker) { if (success) { Logger.Log("MapSharer: Download of map " + sha1 + " completed succesfully."); MapDownloadComplete?.Invoke(null, new SHA1EventArgs(sha1, mapName)); } else { Logger.Log("MapSharer: Download of map " + sha1 + "failed! Reason: " + mapPath); MapDownloadFailed?.Invoke(null, new SHA1EventArgs(sha1, mapName)); } MapDownloadQueue.Remove(sha1); if (MapDownloadQueue.Count > 0) { Logger.Log("MapSharer: Continuing custom map downloads."); object[] array = new object[3]; array[0] = MapDownloadQueue[0]; array[1] = myGameId; array[2] = mapName; Download(array); } } } public static string GetMapFileName(string sha1, string mapName) => mapName.ToWin32FileName() + "_" + sha1; private static string DownloadMain(string sha1, string myGame, string mapName, out bool success) { try { string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom"); string mapFileName = GetMapFileName(sha1, mapName); FileInfo destinationFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($"{mapFileName}.zip")); // This string is up here so we can check that there isn't already a .map file for this download. // This prevents the client from crashing when trying to rename the unzipped file to a duplicate filename. FileInfo newFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($"{mapFileName}.{ClientConfiguration.Instance.MapFileExtension}")); try { destinationFile.Delete(); } catch (Exception ex) { Logger.Log($"MapSharer: Failed to delete existing zip file: {ex.Message}"); } try { newFile.Delete(); } catch (Exception ex) { Logger.Log($"MapSharer: Failed to delete existing map file: {ex.Message}"); } if (string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetMapDBDownloadURL)) { success = false; Logger.Log("MapSharer: Download URL is not configured."); return null; } string url = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.zip", ClientConfiguration.Instance.CnCNetMapDBDownloadURL, myGame, sha1); try { Logger.Log($"MapSharer: Downloading URL: {url}"); new TimedHttpClient(DOWNLOAD_TIMEOUT).DownloadFile(url, destinationFile.FullName); } catch (Exception ex) { success = false; return ex.Message; } destinationFile.Refresh(); if (!destinationFile.Exists) { success = false; return null; } string extractedFile; try { extractedFile = ExtractZipFile(destinationFile.FullName, customMapsDirectory); } catch (Exception ex) { Logger.Log($"MapSharer: Failed to extract map: {ex.Message}"); success = false; return ex.Message; } if (String.IsNullOrEmpty(extractedFile)) { success = false; return null; } try { destinationFile.Delete(); } catch (Exception ex) { Logger.Log($"MapSharer: Failed to delete zip file after extraction: {ex.Message}"); } success = true; return extractedFile; } catch (Exception ex) { Logger.Log($"MapSharer: Map download failed with exception: {ex.Message}"); success = false; return ex.Message; } } class FileToUpload { public FileToUpload() { ContentType = "application/octet-stream"; } public string Name { get; set; } public string Filename { get; set; } public string ContentType { get; set; } public Stream Stream { get; set; } } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/NameValidator.cs ================================================ using System; using System.Linq; using ClientCore; using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet { public enum NameValidationError { None = 0, EmptyName, OffensiveName, FirstCharacterIsNumber, FirstCharacterIsHyphen, InvalidCharacters, TooLong } public static class NameValidator { /// /// Gets the localized error message for a player name validation error. /// /// The validation error. /// Localized error message, or null if the error is None. public static string GetLocalizedPlayerNameErrorMessage(NameValidationError error) { switch (error) { case NameValidationError.None: return null; case NameValidationError.EmptyName: return "Please enter a name.".L10N("Client:ClientCore:EnterAName"); case NameValidationError.OffensiveName: return "Please enter a name that is less offensive.".L10N("Client:ClientCore:NameOffensive"); case NameValidationError.FirstCharacterIsNumber: return "The first character in the player name cannot be a number.".L10N("Client:ClientCore:NameFirstIsNumber"); case NameValidationError.FirstCharacterIsHyphen: return "The first character in the player name cannot be a hyphen ( - ).".L10N("Client:ClientCore:NameFirstIsHyphen"); case NameValidationError.InvalidCharacters: return "Your player name has invalid characters in it.".L10N("Client:ClientCore:NameInvalidChar1") + Environment.NewLine + "Allowed characters are anything from A to Z and numbers.".L10N("Client:ClientCore:NameInvalidChar2"); case NameValidationError.TooLong: return "Your nickname is too long.".L10N("Client:ClientCore:NameTooLong"); default: return null; } } /// /// Gets the localized error message for a game name validation error. /// /// The validation error. /// Localized error message, or null if the error is None. public static string GetLocalizedGameNameErrorMessage(NameValidationError error) { switch (error) { case NameValidationError.None: return null; case NameValidationError.EmptyName: return "Please enter a game name.".L10N("Client:Main:PleaseEnterGameName"); case NameValidationError.OffensiveName: return "Please enter a less offensive game name.".L10N("Client:Main:GameNameOffensiveText"); default: return null; } } /// /// Checks if the player's nickname is valid for CnCNet. /// /// The player name to validate. /// The localized error message if validation fails, otherwise null. /// NameValidationError.None if the nickname is valid, otherwise the specific validation error. public static NameValidationError IsNameValid(string name, out string localizedErrorMessage) { var profanityFilter = new ProfanityFilter(); if (string.IsNullOrEmpty(name)) { localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.EmptyName); return NameValidationError.EmptyName; } if (profanityFilter.IsOffensive(name)) { localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.OffensiveName); return NameValidationError.OffensiveName; } if (int.TryParse(name.Substring(0, 1), out _)) { localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.FirstCharacterIsNumber); return NameValidationError.FirstCharacterIsNumber; } if (name[0] == '-') { localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.FirstCharacterIsHyphen); return NameValidationError.FirstCharacterIsHyphen; } // Check that there are no invalid chars char[] allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_[]|\\{}^`".ToCharArray(); char[] nicknameChars = name.ToCharArray(); foreach (char nickChar in nicknameChars) { if (!allowedCharacters.Contains(nickChar)) { localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.InvalidCharacters); return NameValidationError.InvalidCharacters; } } if (name.Length > ClientConfiguration.Instance.MaxNameLength) { localizedErrorMessage = GetLocalizedPlayerNameErrorMessage(NameValidationError.TooLong); return NameValidationError.TooLong; } localizedErrorMessage = null; return NameValidationError.None; } /// /// Returns player nickname constrained to maximum allowed length and with invalid characters for offline nicknames removed. /// Does not check for offensive words or invalid characters for CnCNet. /// /// Player nickname. /// Player nickname with invalid offline nickname characters removed and constrained to maximum name length. public static string GetValidOfflineName(string name) { char[] disallowedCharacters = ",;".ToCharArray(); string validName = new string(name.Trim().Where(c => !disallowedCharacters.Contains(c)).ToArray()); if (validName.Length > ClientConfiguration.Instance.MaxNameLength) return validName.Substring(0, ClientConfiguration.Instance.MaxNameLength); return validName; } /// /// Checks if a lobby room name is valid. /// /// The lobby name to validate. /// The localized error message if validation fails, otherwise null. /// NameValidationError.None if the name is valid, otherwise the specific validation error. public static NameValidationError IsGameNameValid(string name, out string localizedErrorMessage) { var profanityFilter = new ProfanityFilter(); if (string.IsNullOrEmpty(name)) { localizedErrorMessage = GetLocalizedGameNameErrorMessage(NameValidationError.EmptyName); return NameValidationError.EmptyName; } if (profanityFilter.IsOffensive(name)) { localizedErrorMessage = GetLocalizedGameNameErrorMessage(NameValidationError.OffensiveName); return NameValidationError.OffensiveName; } localizedErrorMessage = null; return NameValidationError.None; } /// /// Sanitizes a lobby name by removing invalid characters. /// /// The lobby room name to sanitize. /// The sanitized lobby room name. public static string GetSanitizedGameName(string name) { // semicolons are used as separators in the protocol return name.Replace(";", string.Empty).Trim(); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/SHA1EventArgs.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.CnCNet { public class SHA1EventArgs : EventArgs { public SHA1EventArgs(string sha1, string mapName) { SHA1 = sha1; MapName = mapName; } public string SHA1 { get; private set; } public string MapName { get; private set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/TimedHttpClient.cs ================================================ #nullable enable using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// An HTTP client wrapper that enforces a per-request timeout covering /// the entire operation, including both connection establishment and /// response body read. /// internal sealed class TimedHttpClient { private static readonly HttpClient sharedHttpClient = new HttpClient() { Timeout = Timeout.InfiniteTimeSpan }; private readonly int timeoutMilliseconds; /// /// The timeout in milliseconds. The entire HTTP operation must complete within this time. /// public TimedHttpClient(int timeoutMilliseconds) { this.timeoutMilliseconds = timeoutMilliseconds; } /// /// Downloads the resource at the specified URL as a string. /// Guaranteed to return or throw within the configured timeout. /// public async Task GetStringAsync(string url) { using var cts = new CancellationTokenSource(timeoutMilliseconds); // GetAsync with the default HttpCompletionOption.ResponseContentRead downloads // the complete response body before completing, so cts.Token covers the full operation. using var response = await sharedHttpClient.GetAsync(url, cts.Token).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } /// /// Synchronous wrapper for . /// public string GetString(string url) => GetStringAsync(url).GetAwaiter().GetResult(); /// /// Downloads the resource at the specified URL as a byte array. /// Guaranteed to return or throw within the configured timeout. /// public async Task GetBytesAsync(string url) { using var cts = new CancellationTokenSource(timeoutMilliseconds); using var response = await sharedHttpClient.GetAsync(url, cts.Token).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } /// /// Synchronous wrapper for . /// public byte[] GetBytes(string url) => GetBytesAsync(url).GetAwaiter().GetResult(); /// /// Downloads the resource at the specified URL and saves it to a file. /// Guaranteed to complete or throw within the configured timeout. /// public async Task DownloadFileAsync(string url, string filePath) { using var cts = new CancellationTokenSource(timeoutMilliseconds); // Use ResponseHeadersRead for streaming to avoid buffering the entire file in memory. using var response = await sharedHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false); response.EnsureSuccessStatusCode(); using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); // CopyToAsync respects the cancellation token between iterations, // ensuring the timeout is enforced throughout the body read. await contentStream.CopyToAsync(fileStream, 81920, cts.Token).ConfigureAwait(false); } /// /// Synchronous wrapper for . /// public void DownloadFile(string url, string filePath) => DownloadFileAsync(url, filePath).GetAwaiter().GetResult(); /// /// Posts the specified content to the URL and returns the response body as a byte array. /// Guaranteed to return or throw within the configured timeout. /// public async Task PostAsync(string url, HttpContent content) { using var cts = new CancellationTokenSource(timeoutMilliseconds); using var response = await sharedHttpClient.PostAsync(url, content, cts.Token).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } /// /// Synchronous wrapper for . /// public byte[] Post(string url, HttpContent content) => PostAsync(url, content).GetAwaiter().GetResult(); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs ================================================ using ClientCore; using DTAClient.Online; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; using System.Linq; namespace DTAClient.Domain.Multiplayer.CnCNet { public class TunnelHandler : GameComponent { /// /// Determines the time between pinging the current tunnel (if it's set). /// private const double CURRENT_TUNNEL_PING_INTERVAL = 20.0; /// /// A reciprocal to the value which determines how frequent the full tunnel /// refresh would be done instead of just pinging the current tunnel (1/N of /// current tunnel ping refreshes would be substituted by a full list refresh). /// Multiply by to get the interval /// between full list refreshes. /// private const uint CYCLES_PER_TUNNEL_LIST_REFRESH = 6; private const int SUPPORTED_TUNNEL_VERSION = 2; private readonly object _refreshLock = new object(); private bool _refreshInProgress = false; public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(wm.Game) { this.wm = wm; this.connectionManager = connectionManager; wm.Game.Components.Add(this); Enabled = false; connectionManager.Connected += ConnectionManager_Connected; connectionManager.Disconnected += ConnectionManager_Disconnected; connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; } public List Tunnels { get; private set; } = new List(); public CnCNetTunnel CurrentTunnel { get; set; } = null; public event EventHandler TunnelsRefreshed; public event EventHandler CurrentTunnelPinged; public event Action TunnelPinged; private WindowManager wm; private CnCNetManager connectionManager; private TimeSpan timeSinceTunnelRefresh = TimeSpan.MaxValue; private uint skipCount = 0; private void DoTunnelPinged(int index) { if (TunnelPinged != null) wm.AddCallback(TunnelPinged, index); } private void DoCurrentTunnelPinged() { if (CurrentTunnelPinged != null) wm.AddCallback(CurrentTunnelPinged, this, EventArgs.Empty); } private void ConnectionManager_Connected(object sender, EventArgs e) => Enabled = true; private void ConnectionManager_ConnectionLost(object sender, Online.EventArguments.ConnectionLostEventArgs e) => Enabled = false; private void ConnectionManager_Disconnected(object sender, EventArgs e) => Enabled = false; private void RefreshTunnelsAsync() { lock (_refreshLock) { if (_refreshInProgress) return; _refreshInProgress = true; } Task.Run(() => { try { List tunnels = RefreshTunnels(); wm.AddCallback(new Action>(HandleRefreshedTunnels), tunnels); } finally { lock (_refreshLock) { _refreshInProgress = false; } } }); } private void HandleRefreshedTunnels(List newTunnels) { if (newTunnels.Count == 0) { TunnelsRefreshed?.Invoke(this, EventArgs.Empty); return; } var existingTunnels = Tunnels.ToDictionary(t => $"{t.Address}:{t.Port}"); var updatedTunnels = new List(); foreach (var newTunnel in newTunnels) { string key = $"{newTunnel.Address}:{newTunnel.Port}"; if (existingTunnels.TryGetValue(key, out var existingTunnel)) { // update existing tunnels existingTunnel.UpdateFrom(newTunnel); updatedTunnels.Add(existingTunnel); } else { // add new tunnels updatedTunnels.Add(newTunnel); } } // remove old tunnels Tunnels = updatedTunnels; TunnelsRefreshed?.Invoke(this, EventArgs.Empty); for (int i = 0; i < Tunnels.Count; i++) { if (UserINISettings.Instance.PingUnofficialCnCNetTunnels || Tunnels[i].Official || Tunnels[i].Recommended) _ = PingListTunnelAsync(i); } if (CurrentTunnel != null) { var updatedTunnel = Tunnels.Find(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); if (updatedTunnel != null) { // don't re-ping if the tunnel still exists in list, just update the tunnel instance and // fire the event handler (the tunnel was already pinged when traversing the tunnel list) CurrentTunnel = updatedTunnel; DoCurrentTunnelPinged(); } else { // tunnel is not in the list anymore so it's not updated with a list instance and pinged PingCurrentTunnelAsync(); } } } private Task PingListTunnelAsync(int index) { return Task.Run(() => { Tunnels[index].UpdatePing(); DoTunnelPinged(index); }); } private Task PingCurrentTunnelAsync(bool checkTunnelList = false) { return Task.Run(() => { var tunnel = CurrentTunnel; if (tunnel == null) return; tunnel.UpdatePing(); DoCurrentTunnelPinged(); if (checkTunnelList) { int tunnelIndex = Tunnels.FindIndex(t => t.Address == tunnel.Address && t.Port == tunnel.Port); if (tunnelIndex > -1) DoTunnelPinged(tunnelIndex); } }); } private bool OnlineTunnelDataAvailable => !string.IsNullOrWhiteSpace(ClientConfiguration.Instance.CnCNetTunnelListURL); private bool OfflineTunnelDataAvailable => SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache").Exists; private byte[] GetRawTunnelDataOnline() { return new TimedHttpClient(10000).GetBytes(ClientConfiguration.Instance.CnCNetTunnelListURL); } private byte[] GetRawTunnelDataOffline() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); return File.ReadAllBytes(tunnelCacheFile.FullName); } private byte[] GetRawTunnelData(int retryCount = 2) { Logger.Log("Fetching tunnel server info."); if (OnlineTunnelDataAvailable) { for (int i = 0; i < retryCount; i++) { try { byte[] data = GetRawTunnelDataOnline(); return data; } catch (Exception ex) { Logger.Log("Error when downloading tunnel server info: " + ex.Message); if (i < retryCount - 1) Logger.Log("Retrying."); else Logger.Log("Fetching tunnel server list failed."); } } } else { // Don't fetch the latest tunnel list if it is explicitly disabled // For example, the official CnCNet server might be unavailable/unstable in a country with Internet censorship, // where players might either establish a substitute server or manually distribute the tunnel cache file Logger.Log("Fetching tunnel server list online is disabled."); } if (OfflineTunnelDataAvailable) { Logger.Log("Using cached tunnel data."); byte[] data = GetRawTunnelDataOffline(); return data; } else Logger.Log("Tunnel cache file doesn't exist!"); return null; } /// /// Downloads and parses the list of CnCNet tunnels. /// /// A list of tunnel servers. private List RefreshTunnels() { List returnValue = new List(); var seenAddresses = new HashSet(StringComparer.OrdinalIgnoreCase); FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); byte[] data = GetRawTunnelData(); if (data is null) return returnValue; string convertedData = Encoding.Default.GetString(data); string[] serverList = convertedData.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); // skip first header item ("address;country;countrycode;name;password;clients;maxclients;official;latitude;longitude;version;distance") foreach (string serverInfo in serverList.Skip(1)) { try { CnCNetTunnel tunnel = CnCNetTunnel.Parse(serverInfo); if (tunnel == null) continue; if (tunnel.RequiresPassword) continue; if (tunnel.Version != SUPPORTED_TUNNEL_VERSION) continue; if (!seenAddresses.Add($"{tunnel.Address}:{tunnel.Port}")) continue; returnValue.Add(tunnel); } catch (Exception ex) { Logger.Log("Caught an exception when parsing a tunnel server: " + ex.ToString()); } } if (returnValue.Count > 0) { try { if (tunnelCacheFile.Exists) tunnelCacheFile.Delete(); DirectoryInfo clientDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); if (!clientDirectoryInfo.Exists) clientDirectoryInfo.Create(); File.WriteAllBytes(tunnelCacheFile.FullName, data); } catch (Exception ex) { Logger.Log("Refreshing tunnel cache file failed! Returned error: " + ex.ToString()); } } Logger.Log($"Successfully refreshed tunnel cache with {returnValue.Count} servers."); return returnValue; } public override void Update(GameTime gameTime) { if (timeSinceTunnelRefresh > TimeSpan.FromSeconds(CURRENT_TUNNEL_PING_INTERVAL)) { if (skipCount % CYCLES_PER_TUNNEL_LIST_REFRESH == 0) { skipCount = 0; RefreshTunnelsAsync(); } else if (CurrentTunnel != null) { PingCurrentTunnelAsync(true); } timeSinceTunnelRefresh = TimeSpan.Zero; skipCount++; } else timeSinceTunnelRefresh += gameTime.ElapsedGameTime; base.Update(gameTime); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CoopHouseInfo.cs ================================================ using Rampastring.Tools; using System.Collections.Generic; using System; namespace DTAClient.Domain.Multiplayer { /// /// Holds information about enemy houses in a co-op map. /// public struct CoopHouseInfo { public CoopHouseInfo(int side, int color, int startingLocation) { Side = side; Color = color; StartingLocation = startingLocation; } /// /// The index of the enemy house's side. /// public int Side; /// /// The index of the enemy house's color. /// public int Color; /// /// The starting location waypoint of the enemy house. /// public int StartingLocation; public static List GetGenericHouseInfoList(IniSection iniSection, string keyName) { var houseList = new List(); for (int i = 0; ; i++) { string[] houseInfo = iniSection.GetStringValue(keyName + i, string.Empty).Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (houseInfo.Length == 0) break; int[] info = Conversions.IntArrayFromStringArray(houseInfo); var chInfo = new CoopHouseInfo(info[0], info[1], info[2]); houseList.Add(new CoopHouseInfo(info[0], info[1], info[2])); } return houseList; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CoopMapInfo.cs ================================================ #nullable enable using System.Collections.Generic; using System.Text.Json.Serialization; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer { public class CoopMapInfo { [JsonInclude] public List EnemyHouses = new List(); [JsonInclude] public List AllyHouses = new List(); [JsonInclude] public List DisallowedPlayerSides = new List(); [JsonInclude] public List DisallowedPlayerColors = new List(); public CoopMapInfo() { } public void Initialize(IniSection section) { DisallowedPlayerSides = section.GetListValue("DisallowedPlayerSides", ',', int.Parse); DisallowedPlayerColors = section.GetListValue("DisallowedPlayerColors", ',', int.Parse); EnemyHouses = CoopHouseInfo.GetGenericHouseInfoList(section, "EnemyHouse"); AllyHouses = CoopHouseInfo.GetGenericHouseInfoList(section, "AllyHouse"); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/CustomMapCache.cs ================================================ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json.Serialization; namespace DTAClient.Domain.Multiplayer { public class CustomMapCache { [JsonInclude] [JsonPropertyName("version")] public required int Version { get; set; } [JsonInclude] [JsonPropertyName("maps")] public required ConcurrentDictionary Items { get; set; } public record Item { [JsonInclude] public required Map Map { get; init; } [JsonInclude] public long FileSize { get; private set; } [JsonInclude] public DateTime LastWriteTimeUtc { get; private set; } public Item() : base() { } [SetsRequiredMembers] public Item(Map map) { Map = map; FileInfo fileInfo = new(Map.CompleteFilePath); if (fileInfo.Exists) { FileSize = fileInfo.Length; LastWriteTimeUtc = fileInfo.LastWriteTimeUtc; } else { FileSize = 0; LastWriteTimeUtc = DateTime.MinValue; } } public bool IsOutdated() { Item refreshedItem = new(Map); return refreshedItem.FileSize != FileSize || refreshedItem.LastWriteTimeUtc != LastWriteTimeUtc; } } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/GameMode.cs ================================================ using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; namespace DTAClient.Domain.Multiplayer { /// /// A multiplayer game mode. /// public class GameMode : GameModeMapBase { public GameMode(string name) { Name = name; Initialize(); } private const string BASE_INI_PATH = "INI/Map Code/"; private const string SPAWN_INI_OPTIONS_SECTION = "ForcedSpawnIniOptions"; /// /// The internal (INI) name of the game mode. /// public string Name { get; set; } /// /// The user-interface name of the game mode. /// public string UIName { get; private set; } /// /// The original user-interface name of the game mode before translation. /// public string UntranslatedUIName { get; private set; } /// /// List of side indices players cannot select in this game mode. /// public List DisallowedPlayerSides = new List(); /// /// List of side indices human players cannot select in this game mode. /// public List DisallowedHumanPlayerSides = new List(); /// /// List of side indices computer players cannot select in this game mode. /// public List DisallowedComputerPlayerSides = new List(); /// /// Override for minimum amount of players needed to play any map in this game mode. /// Priority sequences: GameMode.MinPlayersOverride, Map.MinPlayers, GameMode.MinPlayers. /// public int? MinPlayersOverride { get; private set; } public int? MaxPlayersOverride { get; private set; } private string mapCodeININame; private List randomizedMapCodeININames; private int randomizedMapCodesCount; private string forcedOptionsSection; public List Maps = new List(); public List> ForcedCheckBoxValues = new List>(); public List> ForcedDropDownValues = new List>(); private List> ForcedSpawnIniOptions = new List>(); public void Initialize() { IniFile forcedOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath)); IniSection section = forcedOptionsIni.GetSection(Name) ?? new IniSection(Name); UntranslatedUIName = section.GetStringValue("UIName", Name); UIName = UntranslatedUIName.L10N($"INI:GameModes:{Name}:UIName"); InitializeBaseSettingsFromIniSection(section, isCustomMap: false); MinPlayersOverride = section.GetIntValueOrNull("MinPlayersOverride"); MaxPlayersOverride = section.GetIntValueOrNull("MaxPlayersOverride"); forcedOptionsSection = section.GetStringValue("ForcedOptions", string.Empty); mapCodeININame = section.GetStringValue("MapCodeININame", Name + ".ini"); randomizedMapCodeININames = section.GetStringValue("RandomizedMapCodeININames", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); randomizedMapCodesCount = section.GetIntValue("RandomizedMapCodesCount", 1); DisallowedPlayerSides = section.GetListValue("DisallowedPlayerSides", ',', int.Parse); DisallowedHumanPlayerSides = section.GetListValue("DisallowedHumanPlayerSides", ',', int.Parse); DisallowedComputerPlayerSides = section.GetListValue("DisallowedComputerPlayerSides", ',', int.Parse); ParseForcedOptions(forcedOptionsIni); ParseSpawnIniOptions(forcedOptionsIni); } private void ParseForcedOptions(IniFile forcedOptionsIni) { if (string.IsNullOrEmpty(forcedOptionsSection)) return; List keys = forcedOptionsIni.GetSectionKeys(forcedOptionsSection); if (keys == null) return; foreach (string key in keys) { string value = forcedOptionsIni.GetStringValue(forcedOptionsSection, key, string.Empty); int intValue = 0; if (int.TryParse(value, out intValue)) { ForcedDropDownValues.Add(new KeyValuePair(key, intValue)); } else { ForcedCheckBoxValues.Add(new KeyValuePair(key, Conversions.BooleanFromString(value, false))); } } } private void ParseSpawnIniOptions(IniFile forcedOptionsIni) { string section = forcedOptionsIni.GetStringValue(Name, "ForcedSpawnIniOptions", Name + SPAWN_INI_OPTIONS_SECTION); List spawnIniKeys = forcedOptionsIni.GetSectionKeys(section); if (spawnIniKeys == null) return; foreach (string key in spawnIniKeys) { ForcedSpawnIniOptions.Add(new KeyValuePair(key, forcedOptionsIni.GetStringValue(section, key, string.Empty))); } } public void ApplySpawnIniCode(IniFile spawnIni) { foreach (KeyValuePair key in ForcedSpawnIniOptions) spawnIni.SetStringValue("Settings", key.Key, key.Value); } public List GetMapRulesIniFiles(Random pseudoRandom) { var mapRules = new List() { new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, mapCodeININame)) }; if (randomizedMapCodeININames.Count == 0) return mapRules; Dictionary randomOrder = new(); foreach (string name in randomizedMapCodeININames) { randomOrder[name] = pseudoRandom.Next(); } mapRules.AddRange( from iniName in randomizedMapCodeININames.OrderBy(x => randomOrder[x]).Take(randomizedMapCodesCount) select new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, BASE_INI_PATH, iniName))); return mapRules; } protected bool Equals(GameMode other) => string.Equals(Name, other?.Name, StringComparison.InvariantCultureIgnoreCase); public override int GetHashCode() => (Name != null ? Name.GetHashCode() : 0); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/GameModeMap.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer { /// /// An instance of a Map in a given GameMode /// public record GameModeMap : IGameModeMap { public required GameMode GameMode { get; init; } public required Map Map { get; init; } public bool IsFavorite { get; set; } = false; public GameModeMap() { } [SetsRequiredMembers] public GameModeMap(GameMode gameMode, Map map) { GameMode = gameMode; Map = map; } [SetsRequiredMembers] public GameModeMap(GameMode gameMode, Map map, bool isFavorite) { GameMode = gameMode; Map = map; IsFavorite = isFavorite; } public string ToUntranslatedUIString() => $"{Map.UntranslatedName} - {GameMode.UntranslatedUIName}"; public string ToUIString() => $"{Map.Name} - {GameMode.UIName}"; public override string ToString() => ToUIString(); public List AllowedStartingLocations { get { var ret = Map.AllowedStartingLocations ?? GameMode.AllowedStartingLocations ?? Enumerable.Range(1, MaxPlayers).ToList(); if (ret.Count != MaxPlayers) throw new Exception(string.Format("The number of AllowedStartingLocations does not equal to MaxPlayer.".L10N("Client:Main:InvalidAllowedStartingLocationsCount"))); return ret; } } public int CoopDifficultyLevel => Map.CoopDifficultyLevel ?? GameMode.CoopDifficultyLevel ?? 0; public CoopMapInfo? CoopInfo => Map.CoopInfo ?? GameMode.CoopInfo ?? null; public bool EnforceMaxPlayers => Map.EnforceMaxPlayers ?? GameMode.EnforceMaxPlayers ?? false; public bool ForceNoTeams => Map.ForceNoTeams ?? GameMode.ForceNoTeams ?? false; public bool ForceRandomStartLocations => Map.ForceRandomStartLocations ?? GameMode.ForceRandomStartLocations ?? false; public bool HumanPlayersOnly => Map.HumanPlayersOnly ?? GameMode.HumanPlayersOnly ?? false; public bool IsCoop => Map.IsCoop ?? GameMode.IsCoop ?? false; public int MaxPlayers => // Note: GameLobbyBase.GetMapList() assumes the priority. // If you have modified the expression here, you should also update GameLobbyBase.GetMapList(). GameMode.MaxPlayersOverride ?? Map.MaxPlayers ?? GameMode.MaxPlayers ?? 0; public int MinPlayers => GameMode.MinPlayersOverride ?? Map.MinPlayers ?? GameMode.MinPlayers ?? 0; public bool MultiplayerOnly => Map.MultiplayerOnly ?? GameMode.MultiplayerOnly ?? false; } } ================================================ FILE: DXMainClient/Domain/Multiplayer/GameModeMapBase.cs ================================================ #nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json.Serialization; using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer { public abstract class GameModeMapBase { public const int MAX_PLAYERS = 8; /// /// The maximum amount of players supported by the map or a game mode (such as a 2v2 mode). /// [JsonInclude] public int? MaxPlayers { get; private set; } /// /// The minimum amount of players supported by the map or a game mode. /// [JsonInclude] public int? MinPlayers { get; private set; } /// /// Whether to use MaxPlayers for limiting the player count of the map or a game mode. /// If false (which is the default), MaxPlayers is only used for randomizing /// players to starting waypoints. /// [JsonInclude] public bool? EnforceMaxPlayers { get; private set; } /// /// The allowed starting locations for this map or game mode. /// [JsonInclude] public List? AllowedStartingLocations { get; private set; } /// /// Controls if the map is meant for a co-operation game mode /// (enables briefing logic and forcing options, among others). /// [JsonInclude] public bool? IsCoop { get; private set; } /// /// Contains co-op information. /// [JsonInclude] public CoopMapInfo? CoopInfo { get; private set; } [JsonInclude] public int? CoopDifficultyLevel { get; set; } /// /// If set, this map cannot be played on Skirmish. /// [JsonInclude] public bool? MultiplayerOnly { get; private set; } /// /// If set, this map cannot be played with AI players. /// [JsonInclude] public bool? HumanPlayersOnly { get; private set; } /// /// If set, players are forced to random starting locations on this map. /// [JsonInclude] public bool? ForceRandomStartLocations { get; private set; } /// /// If set, players are forced to different teams on this map. /// [JsonInclude] public bool? ForceNoTeams { get; private set; } protected void InitializeBaseSettingsFromIniSection(IniSection section, bool isCustomMap) { // MinPlayers MinPlayers = section.GetIntValueOrNull(isCustomMap ? "MinPlayer" : "MinPlayers"); // MaxPlayers if (isCustomMap) MaxPlayers = section.GetIntValueOrNull("ClientMaxPlayer") ?? section.GetIntValueOrNull("MaxPlayer"); else MaxPlayers = section.GetIntValueOrNull("MaxPlayers"); // EnforceMaxPlayers EnforceMaxPlayers = section.GetBooleanValueOrNull("EnforceMaxPlayers"); // AllowedStartingLocations List? rawAllowedStartingLocations = section.GetListValueOrNull("AllowedStartingLocations", ',', int.Parse); if (rawAllowedStartingLocations != null && rawAllowedStartingLocations.Count > 0) { // In configuration files, the number starts from 0. While in the code, the number starts from 1. AllowedStartingLocations = rawAllowedStartingLocations.Select(x => x + 1).Distinct().OrderBy(x => x).ToList(); if (AllowedStartingLocations.Max() > MAX_PLAYERS || AllowedStartingLocations.Min() <= 0) throw new Exception(string.Format("Invalid AllowedStartingLocations {0}".L10N("Client:Main:InvalidAllowedStartingLocations"), string.Join(", ", rawAllowedStartingLocations))); } // IsCoop IsCoop = section.GetBooleanValueOrNull("IsCoopMission"); // CoopInfo if (IsCoop ?? false) { CoopInfo = new CoopMapInfo(); CoopInfo.Initialize(section); } // MultiplayerOnly MultiplayerOnly = section.GetBooleanValueOrNull(isCustomMap ? "ClientMultiplayerOnly" : "MultiplayerOnly"); // HumanPlayersOnly HumanPlayersOnly = section.GetBooleanValueOrNull("HumanPlayersOnly"); // ForceRandomStartLocations ForceRandomStartLocations = section.GetBooleanValueOrNull("ForceRandomStartLocations"); // ForceNoTeams ForceNoTeams = section.GetBooleanValueOrNull("ForceNoTeams"); // CoopDifficultyLevel CoopDifficultyLevel = section.GetIntValueOrNull("CoopDifficultyLevel"); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/GameModeMapCollection.cs ================================================ #nullable enable using System; using System.Collections; using System.Collections.Generic; using System.Linq; using ClientCore; namespace DTAClient.Domain.Multiplayer { public class GameModeMapCollection : IReadOnlyGameModeMapCollection { private readonly List items; private readonly Dictionary mapHashIndex; // Note: whenever `items` is modified, we must invalidate the cached GameModes list by setting `_gameModes` to null. private List? _gameModes = null; public IReadOnlyList GameModes => _gameModes ??= items.Select(gmm => gmm.GameMode).Distinct().ToList(); public GameModeMapCollection(IEnumerable gameModes) { // Build the list of GameModeMaps items = gameModes.SelectMany(gm => gm.Maps.Select(map => new GameModeMap(gm, map, UserINISettings.Instance.IsFavoriteMap(map.SHA1, map.UntranslatedName, gm.Name)))) .Distinct() .ToList(); // Build the hash index for fast lookups mapHashIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var gameModeMap in items) { var map = gameModeMap.Map; if (!string.IsNullOrEmpty(map.SHA1) && !mapHashIndex.ContainsKey(map.SHA1)) mapHashIndex[map.SHA1] = map; } } /// /// Finds a map by its SHA1 hash with optimized performance. /// /// The SHA1 hash of the map. /// The map if found, null otherwise. public Map? FindMapByHash(string mapHash) { if (string.IsNullOrEmpty(mapHash)) return null; mapHashIndex.TryGetValue(mapHash, out Map? map); return map; } /// /// Adds the specified game mode map to the collection. /// /// The game mode map to add to the collection. public void Add(GameModeMap gameModeMap) { items.Add(gameModeMap); _gameModes = null; // Update the hash index Map? map = gameModeMap?.Map; if (map != null) { string sha1 = map.SHA1; if (!string.IsNullOrEmpty(sha1) && !mapHashIndex.ContainsKey(sha1)) mapHashIndex[sha1] = map; } } /// /// Adds a range of GameModeMaps to the collection and updates the hash index. /// public void AddRange(IEnumerable gameModeMapCollection) { foreach (var gameModeMap in gameModeMapCollection) Add(gameModeMap); } /// /// Removes a GameModeMap from the collection and updates the hash index if needed. /// public bool Remove(GameModeMap gameModeMap) { bool removed = items.Remove(gameModeMap); if (removed) { _gameModes = null; var map = gameModeMap.Map; // Only remove from index if no other GameModeMap references this map if (!string.IsNullOrEmpty(map.SHA1) && !items.Any(gmm => string.Equals(gmm.Map.SHA1, map.SHA1, StringComparison.OrdinalIgnoreCase))) mapHashIndex.Remove(map.SHA1); } return removed; } public GameModeMap this[int index] => items[index]; public int Count => items.Count; public IEnumerator GetEnumerator() => items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => items.GetEnumerator(); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/GameOptionPresets.cs ================================================ using ClientCore; using Rampastring.Tools; using System; using System.Collections.Generic; using System.Linq; namespace DTAClient.Domain.Multiplayer { /// /// A single game option preset. /// public class GameOptionPreset { public GameOptionPreset(string profileName) { ProfileName = profileName; if (ProfileName.Contains('[') || ProfileName.Contains(']')) throw new ArgumentException("Game option preset name cannot contain the [] characters."); } /// /// Checks if a specific name is valid for the name of a game option preset. /// Returns null if the name is valid, an error message otherwise. /// public static string IsNameValid(string name) { if (name.Contains('[') || name.Contains(']')) return "Game option preset name cannot contain the [] characters."; return null; } public string ProfileName { get; } private Dictionary checkBoxValues = new Dictionary(); private Dictionary dropDownValues = new Dictionary(); private void AddValues(IniSection section, string keyName, Dictionary dictionary, Converter converter) { string[] valueStrings = section.GetStringValue(keyName, string.Empty).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string value in valueStrings) { string[] splitValue = value.Split(':'); if (splitValue.Length != 2) { Logger.Log($"Failed to parse game option preset value ({ProfileName}, {keyName})"); continue; } dictionary.Add(splitValue[0], converter(splitValue[1])); } } public void AddCheckBoxValue(string checkBoxName, bool value) { checkBoxValues.Add(checkBoxName, value); } public void AddDropDownValue(string dropDownValue, int value) { dropDownValues.Add(dropDownValue, value); } public Dictionary GetCheckBoxValues() => new Dictionary(checkBoxValues); public Dictionary GetDropDownValues() => new Dictionary(dropDownValues); public void Read(IniSection section) { // Syntax example: // CheckBoxValues=chkCrates:1,chkShortGame:1,chkFastResourceGrowth:0,.... (0 = unchecked, 1 = checked) // DropDownValues=ddTechLevel:7,ddStartingCredits:5,... (the number is the selected option index) AddValues(section, "CheckBoxValues", checkBoxValues, s => s == "1"); AddValues(section, "DropDownValues", dropDownValues, s => Conversions.IntFromString(s, 0)); } public void Write(IniSection section) { section.SetStringValue("CheckBoxValues", string.Join(",", checkBoxValues.Select(s => $"{s.Key}:{(s.Value ? "1" : "0")}"))); section.SetStringValue("DropDownValues", string.Join(",", dropDownValues.Select(s => $"{s.Key}:{s.Value.ToString()}"))); } } /// /// Handles game option presets. /// public class GameOptionPresets { private const string IniFileName = "GameOptionsPresets.ini"; private const string PresetDefinitionsSectionName = "Presets"; private GameOptionPresets() { } private static GameOptionPresets _instance; public static GameOptionPresets Instance { get { if (_instance == null) _instance = new GameOptionPresets(); return _instance; } } private IniFile gameOptionPresetsIni; private Dictionary presets; public GameOptionPreset GetPreset(string name) { LoadIniIfNotInitialized(); if (presets.TryGetValue(name, out GameOptionPreset value)) return value; return null; } public List GetPresetNames() { LoadIniIfNotInitialized(); return presets.Keys .Where(key => !string.IsNullOrWhiteSpace(key)) .ToList(); } public void AddPreset(GameOptionPreset preset) { LoadIniIfNotInitialized(); presets[preset.ProfileName] = preset; WriteIni(); } public void DeletePreset(string name) { LoadIniIfNotInitialized(); if (!presets.ContainsKey(name)) return; presets.Remove(name); WriteIni(); } private void LoadIniIfNotInitialized() { if (gameOptionPresetsIni == null) LoadIni(); } private void LoadIni() { gameOptionPresetsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, IniFileName)); presets = new Dictionary(); IniSection presetsDefinitions = gameOptionPresetsIni.GetSection(PresetDefinitionsSectionName); if (presetsDefinitions == null) return; foreach (var kvp in presetsDefinitions.Keys) { if (!presets.ContainsKey(kvp.Value)) { IniSection presetSection = gameOptionPresetsIni.GetSection(kvp.Value); if (presetSection == null) continue; var preset = new GameOptionPreset(kvp.Value); preset.Read(presetSection); presets[kvp.Value] = preset; } } } private void WriteIni() { gameOptionPresetsIni = new IniFile(); int i = 0; var definitionsSection = new IniSection(PresetDefinitionsSectionName); gameOptionPresetsIni.AddSection(definitionsSection); foreach (var kvp in presets) { definitionsSection.SetStringValue(i.ToString(), kvp.Value.ProfileName); var presetSection = new IniSection(kvp.Value.ProfileName); kvp.Value.Write(presetSection); gameOptionPresetsIni.AddSection(presetSection); i++; } gameOptionPresetsIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, IniFileName)); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/GenericHostedGame.cs ================================================ using DTAClient.Domain.Multiplayer.CnCNet; using System; namespace DTAClient.Domain.Multiplayer { /// /// A base class for hosted games. /// CnCNet and LAN games derive from this. /// public abstract class GenericHostedGame: IEquatable { public virtual string RoomName { get; set; } public bool Incompatible { get; set; } public bool Locked { get; set; } public bool IsLoadedGame { get; set; } public bool Passworded { get; set; } public CnCNetGame Game { get; set; } public string GameMode { get; set; } public string Map { get; set; } public string MapHash { get; set; } public string GameVersion { get; set; } public string HostName { get; set; } public string[] Players { get; set; } public int MaxPlayers { get; set; } = 8; public abstract int Ping { get; } public DateTime LastRefreshTime { get; set; } public int SkillLevel { get; set; } public virtual bool Equals(GenericHostedGame other) => string.Equals(RoomName, other?.RoomName, StringComparison.InvariantCultureIgnoreCase); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/ICacheManager.cs ================================================ #nullable enable using System; namespace DTAClient.Domain.Multiplayer; public interface ICacheManager : IDisposable { /// /// Gets the number of elements contained in the collection. /// public int Count { get; } /// /// Clears all cached items. The manager does not call Dispose() on the items; it assumes they are managed and will be collected by the garbage collector. /// public void Clear(); /// /// Requests an output to be computed for the specified input. /// /// The input to get the output. /// The cached output if found or computed. /// If true, the method will attempt to compute the output immediately if it's not cached, which may be CPU-intensive. If false, the input will be queued for asynchronous processing if holds. /// This parameter is ignored if is true. Otherwise, if true, the input will be added to the processing queue if not already cached; if false, the method will simply return null on cache miss without queuing. /// True if the output was found in cache or computed synchronously; false if the output is not available yet. public bool Request(TInput input, out TOutput? output, bool syncComputeOnCacheMiss = false, bool addToQueue = true); } ================================================ FILE: DXMainClient/Domain/Multiplayer/IGameModeMap.cs ================================================ #nullable enable using System.Collections.Generic; namespace DTAClient.Domain.Multiplayer { public interface IGameModeMap { public List AllowedStartingLocations { get; } public int CoopDifficultyLevel { get; } public CoopMapInfo? CoopInfo { get; } public bool EnforceMaxPlayers { get; } public bool ForceNoTeams { get; } public bool ForceRandomStartLocations { get; } public bool HumanPlayersOnly { get; } public bool IsCoop { get; } public int MaxPlayers { get; } public int MinPlayers { get; } public bool MultiplayerOnly { get; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/IMapPreviewCacheManager.cs ================================================ #nullable enable using System; using SixLabors.ImageSharp; namespace DTAClient.Domain.Multiplayer; public interface IMapPreviewCacheManager : ICacheManager { } ================================================ FILE: DXMainClient/Domain/Multiplayer/IReadOnlyGameModeMapCollection.cs ================================================ #nullable enable using System.Collections.Generic; namespace DTAClient.Domain.Multiplayer { public interface IReadOnlyGameModeMapCollection : IReadOnlyList { public IReadOnlyList GameModes { get; } public Map? FindMapByHash(string mapHash); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/ClientIntCommandHandler.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.LAN { public class ClientIntCommandHandler : LANClientCommandHandler { public ClientIntCommandHandler(string commandName, Action action) : base(commandName) { this.action = action; } private Action action; public override bool Handle(string message) { if (!message.StartsWith(CommandName)) return false; if (message.Length < CommandName.Length + 2) return false; int value; bool success = int.TryParse(message.Substring(CommandName.Length + 1), out value); if (!success) return false; action(value); return true; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/ClientNoParamCommandHandler.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.LAN { /// /// A command handler that has no parameters. /// class ClientNoParamCommandHandler : LANClientCommandHandler { public ClientNoParamCommandHandler(string commandName, Action commandHandler) : base(commandName) { this.commandHandler = commandHandler; } Action commandHandler; public override bool Handle(string message) { if (message != CommandName) return false; commandHandler(); return true; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/ClientStringCommandHandler.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.LAN { public class ClientStringCommandHandler : LANClientCommandHandler { public ClientStringCommandHandler(string commandName, Action action) : base(commandName) { this.action = action; } Action action; public override bool Handle(string message) { if (!message.StartsWith(CommandName)) return false; action(message.Substring(CommandName.Length + 1)); return true; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/HostedLANGame.cs ================================================ using System; using System.Net; using ClientCore; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using Rampastring.Tools; namespace DTAClient.Domain.LAN { class HostedLANGame : GenericHostedGame { public IPEndPoint EndPoint { get; set; } public override string RoomName { get => HostName + "'s Game" + (EndPoint != null ? " [" + EndPoint.Address.ToString() + "]" : ""); set { // RoomName is generated from HostName and EndPoint. Setting it has no effect. } } public string LoadedGameID { get; set; } public TimeSpan TimeWithoutRefresh { get; set; } public override int Ping { get { return -1; } } public bool SetDataFromStringArray(GameCollection gc, string[] parameters) { if (parameters.Length != 10) { Logger.Log("Ignoring LAN GAME message because of an incorrect number of parameters."); return false; } if (parameters[0] != ProgramConstants.LAN_PROTOCOL_REVISION) return false; GameVersion = parameters[1]; Incompatible = GameVersion != ProgramConstants.GAME_VERSION; Game = gc.GameList.Find(g => g.InternalName.ToUpperInvariant() == parameters[2]); if (Game == null) return false; Map = parameters[3]; GameMode = parameters[4]; LoadedGameID = parameters[5]; string[] players = parameters[6].Split(','); Players = players; if (players.Length == 0) return false; HostName = players[0]; Locked = Conversions.IntFromString(parameters[7], 1) > 0; IsLoadedGame = Conversions.IntFromString(parameters[8], 0) > 0; LastRefreshTime = DateTime.Now; TimeWithoutRefresh = TimeSpan.Zero; // RoomName is now generated from HostName and EndPoint. Setting it has no effect. // RoomName = HostName + "'s Game"; MapHash = parameters[9]; return true; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/LANClientCommandHandler.cs ================================================ namespace DTAClient.Domain.Multiplayer.LAN { public abstract class LANClientCommandHandler { public LANClientCommandHandler(string commandName) { CommandName = commandName; } public string CommandName { get; private set; } public abstract bool Handle(string message); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/LANColor.cs ================================================ using Microsoft.Xna.Framework; namespace DTAClient.Domain.LAN { public class LANColor { public LANColor(string name, Color xnaColor) { Name = name; XNAColor = xnaColor; } public string Name { get; private set; } public Color XNAColor { get; private set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/LANLobbyUser.cs ================================================ using System; using System.Net; using Microsoft.Xna.Framework.Graphics; namespace DTAClient.Domain.Multiplayer.LAN { public class LANLobbyUser { public LANLobbyUser(string name, Texture2D gameTexture, IPEndPoint endPoint) { Name = name; GameTexture = gameTexture; EndPoint = endPoint; } public string Name { get; private set; } public Texture2D GameTexture { get; private set; } public IPEndPoint EndPoint { get; private set; } private readonly object timeWithoutRefreshLock = new(); public TimeSpan TimeWithoutRefresh { get; private set; } public void ClearTimeWithoutRefresh() { lock (timeWithoutRefreshLock) { TimeWithoutRefresh = TimeSpan.Zero; } } public void AddToTimeWithoutRefresh(TimeSpan timeToAdd) { lock (timeWithoutRefreshLock) { TimeWithoutRefresh += timeToAdd; } } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs ================================================ using ClientCore; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System; using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; namespace DTAClient.Domain.Multiplayer.LAN { public class LANPlayerInfo : PlayerInfo { public LANPlayerInfo(Encoding encoding) { this.encoding = encoding; Port = PORT; } public event EventHandler MessageReceived; public event EventHandler ConnectionLost; public event EventHandler PlayerPinged; private const int PORT = 1234; private const int LOBBY_PORT = 1233; private const double SEND_PING_TIMEOUT = 10.0; private const double DROP_TIMEOUT = 20.0; private const int LAN_PING_TIMEOUT = 1000; public TimeSpan TimeSinceLastReceivedMessage { get; set; } public TimeSpan TimeSinceLastSentMessage { get; set; } public TcpClient TcpClient { get; private set; } NetworkStream networkStream; Encoding encoding; string overMessage = string.Empty; public void SetClient(TcpClient client) { if (TcpClient != null) throw new InvalidOperationException("TcpClient has already been set for this LANPlayerInfo!"); TcpClient = client; TcpClient.SendTimeout = 1000; networkStream = client.GetStream(); } /// /// Updates logic timers for the player. /// /// Provides a snapshot of timing values. /// True if the player is still considered connected, otherwise false. public bool Update(GameTime gameTime) { TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime; TimeSinceLastSentMessage += gameTime.ElapsedGameTime; if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) SendMessage("PING"); if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) return false; return true; } public override string IPAddress { get { if (TcpClient != null) return ((IPEndPoint)TcpClient.Client.RemoteEndPoint).Address.ToString(); return base.IPAddress; } set { base.IPAddress = value; //throw new InvalidOperationException("Cannot set LANPlayerInfo's IPAddress!"); } } /// /// Sends a message to the player over the network. /// /// The message to send. public void SendMessage(string message) { byte[] buffer; buffer = encoding.GetBytes(message + ProgramConstants.LAN_MESSAGE_SEPARATOR); try { networkStream.Write(buffer, 0, buffer.Length); networkStream.Flush(); } catch { Logger.Log("Sending message to " + ToString() + " failed!"); } TimeSinceLastSentMessage = TimeSpan.Zero; } public override string ToString() { return Name + " (" + IPAddress + ")"; } /// /// Starts receiving messages from the player asynchronously. /// public void StartReceiveLoop() { Thread thread = new Thread(ReceiveMessages); thread.Start(); } /// /// Receives messages sent by the client, /// and hands them over to another class via an event. /// private void ReceiveMessages() { byte[] message = new byte[1024]; string msg = String.Empty; int bytesRead = 0; NetworkStream ns = TcpClient.GetStream(); while (true) { bytesRead = 0; try { //blocks until a client sends a message bytesRead = ns.Read(message, 0, message.Length); } catch (Exception ex) { //a socket error has occured Logger.Log("Socket error with client " + Name + "; removing. Message: " + ex.ToString()); ConnectionLost?.Invoke(this, EventArgs.Empty); break; } if (bytesRead > 0) { msg = encoding.GetString(message, 0, bytesRead); msg = overMessage + msg; List commands = new List(); while (true) { int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); if (index == -1) { overMessage = msg; break; } else { commands.Add(msg.Substring(0, index)); msg = msg.Substring(index + 1); } } foreach (string cmd in commands) { MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd)); } continue; } ConnectionLost?.Invoke(this, EventArgs.Empty); break; } } public void UpdatePing(WindowManager wm) { using (Ping p = new Ping()) { try { PingReply reply = p.Send(System.Net.IPAddress.Parse(IPAddress), LAN_PING_TIMEOUT); if (reply.Status == IPStatus.Success) Ping = Convert.ToInt32(reply.RoundtripTime); wm.AddCallback(PlayerPinged, this, EventArgs.Empty); } catch (PingException ex) { Logger.Log($"Caught an exception when pinging {Name} LAN player: {ex.ToString()}"); } } } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/LANServerCommandHandler.cs ================================================ namespace DTAClient.Domain.Multiplayer.LAN { public abstract class LANServerCommandHandler { public LANServerCommandHandler(string commandName) { CommandName = commandName; } public string CommandName { get; private set; } public abstract bool Handle(LANPlayerInfo pInfo, string message); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/NetworkMessageEventArgs.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.LAN { public class NetworkMessageEventArgs : EventArgs { public NetworkMessageEventArgs(string message) { Message = message; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/ServerNoParamCommandHandler.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.LAN { public class ServerNoParamCommandHandler : LANServerCommandHandler { public ServerNoParamCommandHandler(string commandName, Action handler) : base(commandName) { this.handler = handler; } Action handler; public override bool Handle(LANPlayerInfo pInfo, string message) { if (message == CommandName) { handler(pInfo); return true; } return false; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/LAN/ServerStringCommandHandler.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer.LAN { public class ServerStringCommandHandler : LANServerCommandHandler { public ServerStringCommandHandler(string commandName, Action handler) : base(commandName) { this.handler = handler; } Action handler; public override bool Handle(LANPlayerInfo pInfo, string message) { if (!message.StartsWith(CommandName) || message.Length <= CommandName.Length + 1) return false; handler(pInfo, message); return true; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/Map.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Json.Serialization; using ClientCore; using ClientCore.Extensions; using DTAClient.DXGUI.Multiplayer.GameLobby; using Rampastring.Tools; using SixLabors.ImageSharp; using Point = Microsoft.Xna.Framework.Point; namespace DTAClient.Domain.Multiplayer { public struct ExtraMapPreviewTexture { public string TextureName; public Point Point; public int Level; public bool Toggleable; public ExtraMapPreviewTexture(string textureName, Point point, int level, bool toggleable) { TextureName = textureName; Point = point; Level = level; Toggleable = toggleable; } } /// /// A multiplayer map. /// public class Map : GameModeMapBase { [JsonConstructor] public Map(string baseFilePath) : this(baseFilePath, true) { } public Map(string baseFilePath, bool isCustomMap) { if (string.IsNullOrWhiteSpace(baseFilePath)) throw new ArgumentNullException(nameof(baseFilePath)); Debug.Assert(!baseFilePath.EndsWith($".{ClientConfiguration.Instance.MapFileExtension}", StringComparison.InvariantCultureIgnoreCase), $"Unexpected map path {baseFilePath}. It should not end with the map extension."); BaseFilePath = baseFilePath; customMapFilePath = isCustomMap ? SafePath.CombineFilePath(ProgramConstants.GamePath, FormattableString.Invariant($"{baseFilePath}.{ClientConfiguration.Instance.MapFileExtension}")) : null; Official = string.IsNullOrWhiteSpace(customMapFilePath); } /// /// The name of the map. /// [JsonIgnore] public string Name => !Official || string.IsNullOrEmpty(UntranslatedName) || string.IsNullOrEmpty(BaseFilePath) ? UntranslatedName : UntranslatedName.L10N($"INI:Maps:{BaseFilePath}:Description"); /// /// The original untranslated name of the map. /// [JsonInclude] public string UntranslatedName { get => field; private set { field = value; // Force triggering localization of the name now _ = Name; } } /// /// If set, this map won't be automatically transferred over CnCNet when /// a player doesn't have it. /// [JsonIgnore] public bool Official { get; private set; } /// /// The briefing of the map. /// [JsonInclude] public string Briefing { get; private set; } /// /// The author of the map. /// [JsonInclude] public string Author { get; private set; } /// /// The calculated SHA1 hash of the map. /// [JsonInclude] public string SHA1 { get; private set; } = null; /// /// The path to the map file. /// [JsonInclude] public string BaseFilePath { get; private set; } /// /// Returns the complete path to the map file. /// Includes the game directory in the path. /// [JsonIgnore] public string CompleteFilePath => SafePath.CombineFilePath(ProgramConstants.GamePath, FormattableString.Invariant($"{BaseFilePath}.{ClientConfiguration.Instance.MapFileExtension}")); /// /// The file name of the preview image. /// [JsonInclude] public string PreviewPath { get; private set; } /// /// The game modes that the map is listed for. /// [JsonInclude] public string[] GameModes; /// /// The forced UnitCount for the map. -1 means none. /// [JsonInclude] public int UnitCount = -1; /// /// The forced starting credits for the map. -1 means none. /// [JsonInclude] public int Credits = -1; [JsonInclude] public int NeutralHouseColor = -1; [JsonInclude] public int SpecialHouseColor = -1; [JsonInclude] public int Bases = -1; [JsonInclude] public string[] localSize; [JsonInclude] public string[] actualSize; [JsonInclude] public int x; [JsonInclude] public int y; [JsonInclude] public int width; [JsonInclude] public int height; /// /// The full path of custom map INI file. It gets re-initialized in JsonConstructor, so it won't be serialized / deserialized directly. /// [JsonIgnore] private readonly string customMapFilePath; [JsonInclude] public List waypoints = new List(); /// /// The pixel coordinates of the map's player starting locations. /// [JsonInclude] public List startingLocations; [JsonInclude] public List TeamStartMappingPresets = new List(); [JsonIgnore] public List TeamStartMappings => TeamStartMappingPresets?.FirstOrDefault()?.TeamStartMappings; public void CalculateSHA() { SHA1 = Utilities.CalculateSHA1ForFile(CompleteFilePath); } [JsonInclude] public List> ForcedCheckBoxValues = new List>(0); [JsonInclude] public List> ForcedDropDownValues = new List>(0); [JsonIgnore] private List extraTextures = new List(0); public List GetExtraMapPreviewTextures() => extraTextures; [JsonIgnore] private List> ForcedSpawnIniOptions = new List>(0); /// /// The name of an extra INI file in INI\Map Code\ that should be /// embedded into this map's INI code when a game is started. /// [JsonInclude] public string ExtraININame { get; private set; } /// /// This is used to load a map from the MPMaps.ini (default name) file. /// /// The configuration file for the multiplayer maps. /// True if loading the map succeeded, otherwise false. public bool InitializeFromMpMapsINI(IniFile iniFile) { try { string baseSectionName = iniFile.GetStringValue(BaseFilePath, "BaseSection", string.Empty); if (!string.IsNullOrEmpty(baseSectionName)) iniFile.CombineSections(baseSectionName, BaseFilePath); var section = iniFile.GetSection(BaseFilePath); UntranslatedName = section.GetStringValue("Description", "Unnamed map"); Author = section.GetStringValue("Author", "Unknown author"); GameModes = section.GetStringValue("GameModes", "Default").Split(','); // Initialize PreviewPath { FileInfo mapFile = SafePath.GetFile(BaseFilePath); string previewPath = SafePath.CombineFilePath(SafePath.GetDirectory(mapFile.FullName).Parent.FullName[ProgramConstants.GamePath.Length..], FormattableString.Invariant($"{section.GetStringValue("PreviewImage", mapFile.Name)}.png")); if (!SafePath.GetFile(ProgramConstants.GamePath, previewPath).Exists) previewPath = null; PreviewPath = previewPath; } Briefing = section.GetStringValue("Briefing", string.Empty) .FromIniString() .L10N($"INI:Maps:{BaseFilePath}:Briefing"); CalculateSHA(); InitializeBaseSettingsFromIniSection(section, isCustomMap: false); Credits = section.GetIntValue("Credits", -1); UnitCount = section.GetIntValue("UnitCount", -1); NeutralHouseColor = section.GetIntValue("NeutralColor", -1); SpecialHouseColor = section.GetIntValue("SpecialColor", -1); string bases = section.GetStringValue("Bases", string.Empty); if (!string.IsNullOrEmpty(bases)) { Bases = Convert.ToInt32(Conversions.BooleanFromString(bases, false)); } int i = 0; while (true) { // Format example: // ExtraTexture0=oilderrick.png,200,150,1,false // Third value is optional map cell level, defaults to 0 if unspecified. // Fourth value is optional boolean value that determines if the texture can be toggled on / off. string value = section.GetStringValue("ExtraTexture" + i, null); if (string.IsNullOrWhiteSpace(value)) break; string[] parts = value.Split(','); if (parts.Length is < 3 or > 5) { Logger.Log($"Invalid format for ExtraTexture{i} in map " + BaseFilePath); continue; } bool success = int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int x); success &= int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int y); int level = 0; bool toggleable = false; if (parts.Length > 3) int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out level); if (parts.Length > 4) toggleable = Conversions.BooleanFromString(parts[4], false); extraTextures.Add(new ExtraMapPreviewTexture(parts[0], new Point(x, y), level, toggleable)); i++; } if (MainClientConstants.USE_ISOMETRIC_CELLS) { localSize = section.GetStringValue("LocalSize", "0,0,0,0").Split(','); actualSize = section.GetStringValue("Size", "0,0,0,0").Split(','); } else { x = section.GetIntValue("X", 0); y = section.GetIntValue("Y", 0); width = section.GetIntValue("Width", 0); height = section.GetIntValue("Height", 0); } for (i = 0; i < MAX_PLAYERS; i++) { string waypoint = section.GetStringValue("Waypoint" + i, string.Empty); if (string.IsNullOrEmpty(waypoint)) break; Debug.Assert(int.TryParse(waypoint.Split(',')[0], out _), $"waypoint should be a number, got {waypoint}"); waypoints.Add(waypoint); } GetTeamStartMappingPresets(section); // Parse forced options string forcedOptionsSections = iniFile.GetStringValue(BaseFilePath, "ForcedOptions", string.Empty); if (!string.IsNullOrEmpty(forcedOptionsSections)) { string[] sections = forcedOptionsSections.Split(','); foreach (string foSection in sections) ParseForcedOptions(iniFile, foSection); } string forcedSpawnIniOptionsSections = iniFile.GetStringValue(BaseFilePath, "ForcedSpawnIniOptions", string.Empty); if (!string.IsNullOrEmpty(forcedSpawnIniOptionsSections)) { string[] sections = forcedSpawnIniOptionsSections.Split(','); foreach (string fsioSection in sections) ParseSpawnIniOptions(iniFile, fsioSection); } ExtraININame = section.GetStringValueOrNull("ExtraININame"); return true; } catch (Exception ex) { Logger.Log("Setting info for " + BaseFilePath + " failed! Reason: " + ex.ToString()); PreStartup.LogException(ex); return false; } } private void GetTeamStartMappingPresets(IniSection section) { TeamStartMappingPresets = new List(); for (int i = 0; ; i++) { try { var teamStartMappingPreset = section.GetStringValue($"TeamStartMapping{i}", string.Empty); if (string.IsNullOrEmpty(teamStartMappingPreset)) return; // mapping not found var teamStartMappingPresetName = section.GetStringValue($"TeamStartMapping{i}Name", string.Empty); if (string.IsNullOrEmpty(teamStartMappingPresetName)) continue; // mapping found, but no name specified TeamStartMappingPresets.Add(new TeamStartMappingPreset() { Name = teamStartMappingPresetName, TeamStartMappings = TeamStartMapping.FromListString(teamStartMappingPreset) }); } catch (Exception ex) { Logger.Log($"Unable to parse team start mappings. Map: \"{Name}\", Error: {ex.Message}"); TeamStartMappingPresets = new List(); } } } public List GetStartingLocationPreviewCoords(Point previewSize) { if (startingLocations == null) { startingLocations = new List(); foreach (string waypoint in waypoints) { if (MainClientConstants.USE_ISOMETRIC_CELLS) startingLocations.Add(GetIsometricWaypointCoords(waypoint, actualSize, localSize, previewSize)); else startingLocations.Add(GetTDRAWaypointCoords(waypoint, x, y, width, height, previewSize)); } } return startingLocations; } public Point MapPointToMapPreviewPoint(Point mapPoint, Point previewSize, int level) { if (MainClientConstants.USE_ISOMETRIC_CELLS) return GetIsoTilePixelCoord(mapPoint.X, mapPoint.Y, actualSize, localSize, previewSize, level); return GetTDRACellPixelCoord(mapPoint.X, mapPoint.Y, x, y, width, height, previewSize); } /// Returns the loaded INI file of a custom map. private IniFile GetCustomMapIniFile(bool loadPreviewTextureSection = true) { var customMapIni = new IniFile { FileName = SafePath.CombineFilePath(customMapFilePath) }; customMapIni.AddSection("Basic"); customMapIni.AddSection("Map"); customMapIni.AddSection("Waypoints"); customMapIni.AddSection("ForcedOptions"); customMapIni.AddSection("ForcedSpawnIniOptions"); // Optionally load preview sections, to accelerate building custom map caches without reading preview. if (loadPreviewTextureSection) { customMapIni.AddSection("Preview"); customMapIni.AddSection("PreviewPack"); } customMapIni.AllowNewSections = false; customMapIni.Parse(); return customMapIni; } /// /// Loads map information from a TS/RA2 map INI file. /// Returns true if successful, otherwise false. /// public bool InitializeFromCustomMap() { if (!File.Exists(customMapFilePath)) return false; try { IniFile iniFile = GetCustomMapIniFile(loadPreviewTextureSection: false); IniSection basicSection = iniFile.GetSection("Basic"); UntranslatedName = basicSection.GetStringValue("Name", "Unnamed map"); Author = basicSection.GetStringValue("Author", "Unknown author"); string gameModesString = basicSection.GetStringValue("GameModes", string.Empty); if (string.IsNullOrEmpty(gameModesString)) { gameModesString = basicSection.GetStringValue("GameMode", "Default"); } GameModes = gameModesString.Split(','); if (GameModes.Length == 0) { Logger.Log("Custom map " + customMapFilePath + " has no game modes!"); return false; } for (int i = 0; i < GameModes.Length; i++) { string gameMode = GameModes[i].Trim(); GameModes[i] = gameMode.Substring(0, 1).ToUpperInvariant() + gameMode.Substring(1); } Briefing = basicSection.GetStringValue("Briefing", string.Empty) .FromIniString(); CalculateSHA(); InitializeBaseSettingsFromIniSection(basicSection, isCustomMap: true); Credits = basicSection.GetIntValue("Credits", -1); UnitCount = basicSection.GetIntValue("UnitCount", -1); NeutralHouseColor = basicSection.GetIntValue("NeutralColor", -1); SpecialHouseColor = basicSection.GetIntValue("SpecialColor", -1); // Initialize PreviewPath { string previewPath = Path.ChangeExtension(customMapFilePath[ProgramConstants.GamePath.Length..], ".png"); if (!SafePath.GetFile(ProgramConstants.GamePath, previewPath).Exists) previewPath = null; PreviewPath = previewPath; } string bases = basicSection.GetStringValue("Bases", string.Empty); if (!string.IsNullOrEmpty(bases)) { Bases = Convert.ToInt32(Conversions.BooleanFromString(bases, false)); } localSize = iniFile.GetStringValue("Map", "LocalSize", "0,0,0,0").Split(','); actualSize = iniFile.GetStringValue("Map", "Size", "0,0,0,0").Split(','); if (MainClientConstants.USE_ISOMETRIC_CELLS) { localSize = iniFile.GetStringValue("Map", "LocalSize", "0,0,0,0").Split(','); actualSize = iniFile.GetStringValue("Map", "Size", "0,0,0,0").Split(','); } else { x = iniFile.GetIntValue("Map", "X", 0); y = iniFile.GetIntValue("Map", "Y", 0); width = iniFile.GetIntValue("Map", "Width", 0); height = iniFile.GetIntValue("Map", "Height", 0); } for (int i = 0; i < MAX_PLAYERS; i++) { string waypoint = iniFile.GetStringValue("Waypoints", i.ToString(CultureInfo.InvariantCulture), string.Empty); if (string.IsNullOrEmpty(waypoint)) break; waypoints.Add(waypoint); } GetTeamStartMappingPresets(basicSection); ParseForcedOptions(iniFile, "ForcedOptions"); ParseSpawnIniOptions(iniFile, "ForcedSpawnIniOptions"); ExtraININame = basicSection.GetStringValueOrNull("ExtraININame"); return true; } catch { Logger.Log("Loading custom map " + customMapFilePath + " failed!"); return false; } } // Ran after the map has been loaded from cache if it is a custom map. public void AfterDeserialize(bool recalculateSHA = true) { if (recalculateSHA) { // Instead of doing so, we should just remove the Map object from cache when the map file changes. // Otherwise, the metadata can be out of date. Debug.Assert(false, "The map SHA1 should not be recalculated after deserialization. Remove the Map object from cache when the map file changes instead."); CalculateSHA(); } } private void ParseForcedOptions(IniFile iniFile, string forcedOptionsSection) { List keys = iniFile.GetSectionKeys(forcedOptionsSection); if (keys == null) { Logger.Log("Invalid ForcedOptions section \"" + forcedOptionsSection + "\" in map " + BaseFilePath); return; } foreach (string key in keys) { string value = iniFile.GetStringValue(forcedOptionsSection, key, string.Empty); if (int.TryParse(value, out int intValue)) { ForcedDropDownValues.Add(new KeyValuePair(key, intValue)); } else { ForcedCheckBoxValues.Add(new KeyValuePair(key, Conversions.BooleanFromString(value, false))); } } } private void ParseSpawnIniOptions(IniFile forcedOptionsIni, string spawnIniOptionsSection) { List spawnIniKeys = forcedOptionsIni.GetSectionKeys(spawnIniOptionsSection); foreach (string key in spawnIniKeys) { ForcedSpawnIniOptions.Add(new KeyValuePair(key, forcedOptionsIni.GetStringValue(spawnIniOptionsSection, key, string.Empty))); } } public bool IsImmediatePreviewImageAvailable() => !string.IsNullOrWhiteSpace(PreviewPath) && SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).Exists; public Image GetImmediatePreviewImage() => IsImmediatePreviewImageAvailable() ? Image.Load(SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).FullName) : throw new FileNotFoundException("Immediate preview texture not found for map " + BaseFilePath); public bool IsNonImmediatePreviewImageAvailable() => !string.IsNullOrWhiteSpace(customMapFilePath) && File.Exists(customMapFilePath); public Image GetNonImmediatePreviewImage() { if (!IsNonImmediatePreviewImageAvailable()) throw new FileNotFoundException("Custom map file not found for map " + BaseFilePath); // Debug.WriteLine("Loading map preview from custom map INI for map " + BaseFilePath); return MapPreviewExtractor.ExtractMapPreview(GetCustomMapIniFile(loadPreviewTextureSection: true)); } public IniFile GetMapIni() { Encoding mapIniEncoding = MapCodeHelper.GetMapEncoding(CompleteFilePath); var mapIni = new IniFile(CompleteFilePath, mapIniEncoding); if (!string.IsNullOrEmpty(ExtraININame)) { string extraIniPath = SafePath.CombineFilePath(ProgramConstants.GamePath, "INI", "Map Code", ExtraININame); Encoding extraIniEncoding = MapCodeHelper.GetMapEncoding(extraIniPath); var extraIni = new IniFile(extraIniPath, extraIniEncoding); IniFile.ConsolidateIniFiles(mapIni, extraIni); } return mapIni; } public void ApplySpawnIniCode(IniFile spawnIni, int totalPlayerCount, int aiPlayerCount, bool isCoop, CoopMapInfo coopInfo, int coopDifficultyLevel, Random pseudoRandom, int sideCount) { foreach (KeyValuePair key in ForcedSpawnIniOptions) spawnIni.SetStringValue("Settings", key.Key, key.Value); if (Credits != -1) spawnIni.SetIntValue("Settings", "Credits", Credits); if (UnitCount != -1) spawnIni.SetIntValue("Settings", "UnitCount", UnitCount); int neutralHouseIndex = totalPlayerCount + 1; int specialHouseIndex = totalPlayerCount + 2; if (isCoop) { int NextRandomSide() => pseudoRandom.Next(0, sideCount); var allyHouses = coopInfo.AllyHouses; var enemyHouses = coopInfo.EnemyHouses; int multiId = totalPlayerCount + 1; foreach (var houseInfo in allyHouses.Concat(enemyHouses)) { spawnIni.SetIntValue("HouseHandicaps", "Multi" + multiId, coopDifficultyLevel); spawnIni.SetIntValue("HouseCountries", "Multi" + multiId, houseInfo.Side == -1 ? NextRandomSide() : houseInfo.Side); spawnIni.SetIntValue("HouseColors", "Multi" + multiId, houseInfo.Color); spawnIni.SetIntValue("SpawnLocations", "Multi" + multiId, houseInfo.StartingLocation); multiId++; } for (int i = 0; i < allyHouses.Count; i++) { int aMultiId = totalPlayerCount + i + 1; int allyIndex = 0; // Write alliances for (int pIndex = 0; pIndex < totalPlayerCount + allyHouses.Count; pIndex++) { int allyMultiIndex = pIndex; if (pIndex == aMultiId - 1) continue; spawnIni.SetIntValue("Multi" + aMultiId + "_Alliances", "HouseAlly" + HouseAllyIndexToString(allyIndex), allyMultiIndex); spawnIni.SetIntValue("Multi" + (allyMultiIndex + 1) + "_Alliances", "HouseAlly" + HouseAllyIndexToString(totalPlayerCount + i - 1), aMultiId - 1); allyIndex++; } } for (int i = 0; i < enemyHouses.Count; i++) { int eMultiId = totalPlayerCount + allyHouses.Count + i + 1; int allyIndex = 0; // Write alliances for (int enemyIndex = 0; enemyIndex < enemyHouses.Count; enemyIndex++) { int allyMultiIndex = totalPlayerCount + allyHouses.Count + enemyIndex; if (enemyIndex == i) continue; spawnIni.SetIntValue("Multi" + eMultiId + "_Alliances", "HouseAlly" + HouseAllyIndexToString(allyIndex), allyMultiIndex); allyIndex++; } } spawnIni.SetIntValue("Settings", "AIPlayers", aiPlayerCount + allyHouses.Count + enemyHouses.Count); neutralHouseIndex += allyHouses.Count + enemyHouses.Count; specialHouseIndex += allyHouses.Count + enemyHouses.Count; } if (NeutralHouseColor > -1) spawnIni.SetIntValue("HouseColors", "Multi" + neutralHouseIndex, NeutralHouseColor); if (SpecialHouseColor > -1) spawnIni.SetIntValue("HouseColors", "Multi" + specialHouseIndex, SpecialHouseColor); if (Bases > -1) spawnIni.SetBooleanValue("Settings", "Bases", Convert.ToBoolean(Bases)); } private static string HouseAllyIndexToString(int index) { string[] houseAllyIndexStrings = new string[] { "One", "Two", "Three", "Four", "Five", "Six", "Seven" }; return houseAllyIndexStrings[index]; } public string GetSizeString() { if (MainClientConstants.USE_ISOMETRIC_CELLS) { if (actualSize == null || actualSize.Length < 4) return "Not available"; return actualSize[2] + "x" + actualSize[3]; } else { return width + "x" + height; } } private static Point GetTDRAWaypointCoords(string waypoint, int x, int y, int width, int height, Point previewSizePoint) { int waypointCoordsInt = Conversions.IntFromString(waypoint, -1); if (waypointCoordsInt < 0) return new Point(0, 0); // https://modenc.renegadeprojects.com/Waypoints int waypointX = waypointCoordsInt % MainClientConstants.TDRA_WAYPOINT_COEFFICIENT; int waypointY = waypointCoordsInt / MainClientConstants.TDRA_WAYPOINT_COEFFICIENT; return GetTDRACellPixelCoord(waypointX, waypointY, x, y, width, height, previewSizePoint); } private static Point GetTDRACellPixelCoord(int cellX, int cellY, int x, int y, int width, int height, Point previewSizePoint) { int rx = cellX - x; int ry = cellY - y; double ratioX = rx / (double)width; double ratioY = ry / (double)height; int pixelX = (int)(ratioX * previewSizePoint.X); int pixelY = (int)(ratioY * previewSizePoint.Y); return new Point(pixelX, pixelY); } /// /// Converts a waypoint's coordinate string into pixel coordinates on the preview image. /// /// The waypoint's location on the map preview as a point. private static Point GetIsometricWaypointCoords(string waypoint, string[] actualSizeValues, string[] localSizeValues, Point previewSizePoint) { string[] parts = waypoint.Split(','); int xCoordIndex = parts[0].Length - 3; int isoTileY = Convert.ToInt32(parts[0].Substring(0, xCoordIndex), CultureInfo.InvariantCulture); int isoTileX = Convert.ToInt32(parts[0].Substring(xCoordIndex), CultureInfo.InvariantCulture); int level = 0; if (parts.Length > 1) level = Conversions.IntFromString(parts[1], 0); return GetIsoTilePixelCoord(isoTileX, isoTileY, actualSizeValues, localSizeValues, previewSizePoint, level); } private static Point GetIsoTilePixelCoord(int isoTileX, int isoTileY, string[] actualSizeValues, string[] localSizeValues, Point previewSizePoint, int level) { int rx = isoTileX - isoTileY + Convert.ToInt32(actualSizeValues[2], CultureInfo.InvariantCulture) - 1; int ry = isoTileX + isoTileY - Convert.ToInt32(actualSizeValues[2], CultureInfo.InvariantCulture) - 1; int pixelPosX = rx * MainClientConstants.MAP_CELL_SIZE_X / 2; int pixelPosY = ry * MainClientConstants.MAP_CELL_SIZE_Y / 2 - level * MainClientConstants.MAP_CELL_SIZE_Y / 2; pixelPosX = pixelPosX - (Convert.ToInt32(localSizeValues[0], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_X); pixelPosY = pixelPosY - (Convert.ToInt32(localSizeValues[1], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_Y); // Calculate map size int mapSizeX = Convert.ToInt32(localSizeValues[2], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_X; int mapSizeY = Convert.ToInt32(localSizeValues[3], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_Y; double ratioX = Convert.ToDouble(pixelPosX) / mapSizeX; double ratioY = Convert.ToDouble(pixelPosY) / mapSizeY; int pixelX = Convert.ToInt32(ratioX * previewSizePoint.X); int pixelY = Convert.ToInt32(ratioY * previewSizePoint.Y); return new Point(pixelX, pixelY); } /// /// Opens the folder containing this map in the system file manager and selects the map file. /// public void OpenContainingFolder() { FileInfo mapFileInfo = SafePath.GetFile(CompleteFilePath); if (!mapFileInfo.Exists) return; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // https://stackoverflow.com/questions/13680415/how-to-open-explorer-with-a-specific-file-selected ProcessLauncher.StartShellProcess("explorer.exe", $"/select,\"{mapFileInfo.FullName}\""); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // https://stackoverflow.com/questions/39214539/opening-finder-from-terminal-with-file-selected ProcessLauncher.StartShellProcess("open", $"-R \"{mapFileInfo.FullName}\""); } else { // Linux: no standard way to select a file, just open the folder ProcessLauncher.StartShellProcess(mapFileInfo.Directory?.FullName); } } public override bool Equals(object other) { if (other is Map otherMap) { Debug.Assert(otherMap?.SHA1 != null || SHA1 != null); return string.Equals(SHA1, otherMap?.SHA1, StringComparison.InvariantCultureIgnoreCase); } return false; } public override int GetHashCode() => SHA1 != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(SHA1) : 0; public static bool operator ==(Map left, Map right) => left is null ? right is null : left.Equals(right); public static bool operator !=(Map left, Map right) => !(left == right); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MapChangeEventArgs.cs ================================================ using System; namespace DTAClient.Domain.Multiplayer; public class MapChangedEventArgs : EventArgs { public Map Map { get; set; } public MapChangeType ChangeType { get; set; } public string PreviousMapSHA1 { get; set; } public MapChangedEventArgs(Map map, MapChangeType changeType, string previousMapSHA1 = null) { Map = map; ChangeType = changeType; PreviousMapSHA1 = previousMapSHA1; } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MapFileEventArgs.cs ================================================ using System; using System.IO; namespace DTAClient.Domain.Multiplayer; public class MapFileEventArgs : EventArgs { public string FilePath { get; set; } public string FileName { get; set; } public WatcherChangeTypes ChangeType { get; set; } public string OldFilePath { get; set; } public MapFileEventArgs(string filePath, WatcherChangeTypes changeType, string oldFilePath = null) { FilePath = filePath; FileName = Path.GetFileNameWithoutExtension(filePath); ChangeType = changeType; OldFilePath = oldFilePath; } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MapFileWatcher.cs ================================================ using System; using System.IO; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer; public class MapFileWatcher { private readonly string mapsDirectory; private readonly string mapFileExtension; private FileSystemWatcher fileSystemWatcher; public event EventHandler MapFileChanged; public MapFileWatcher(string mapsPath, string fileExtension) { mapsDirectory = mapsPath; mapFileExtension = fileExtension; } public void StartWatching() { if (fileSystemWatcher != null) return; DirectoryInfo directoryInfo = SafePath.GetDirectory(mapsDirectory); if (!directoryInfo.Exists) return; try { fileSystemWatcher = new FileSystemWatcher(mapsDirectory, $"*.{mapFileExtension}") { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size, IncludeSubdirectories = true }; fileSystemWatcher.Created += OnFileSystemEvent; fileSystemWatcher.Changed += OnFileSystemEvent; fileSystemWatcher.Deleted += OnFileSystemEvent; fileSystemWatcher.Renamed += OnFileRenamed; fileSystemWatcher.EnableRaisingEvents = true; Logger.Log($"MapFileWatcher: Started watching {mapsDirectory} for *.{mapFileExtension} files"); } catch (Exception ex) { Logger.Log($"MapFileWatcher: Failed to start watching directory {mapsDirectory}: {ex.Message}"); fileSystemWatcher?.Dispose(); fileSystemWatcher = null; } } private void OnFileSystemEvent(object sender, FileSystemEventArgs e) { ProcessFileEvent(e.FullPath, e.ChangeType); } private void OnFileRenamed(object sender, RenamedEventArgs e) { // delete + create ProcessFileEvent(e.OldFullPath, WatcherChangeTypes.Deleted); ProcessFileEvent(e.FullPath, WatcherChangeTypes.Created); } private void ProcessFileEvent(string filePath, WatcherChangeTypes changeType) { try { var eventArgs = new MapFileEventArgs(filePath, changeType); MapFileChanged?.Invoke(this, eventArgs); } catch (Exception ex) { Logger.Log($"MapFileWatcher: Error processing file event for {filePath}: {ex.Message}"); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MapLoader.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; using SixLabors.ImageSharp; namespace DTAClient.Domain.Multiplayer { public enum MapChangeType { Added, Updated, Removed } public class MapLoader : IDisposable { private const string CUSTOM_MAPS_DIRECTORY = "Maps/Custom"; private const int CurrentCustomMapCacheVersion = 5; private static string GetCustomMapCacheFileName(int version) => version == 1 ? "custom_map_cache" : $"custom_map_cache_v{version}"; private static readonly string CUSTOM_MAPS_CACHE = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, GetCustomMapCacheFileName(CurrentCustomMapCacheVersion)); private static readonly IReadOnlyList LEGACY_CUSTOM_MAP_CACHE_FILES = Enumerable.Range(0, CurrentCustomMapCacheVersion) .Select(version => SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, GetCustomMapCacheFileName(version))) .ToList(); private const string MultiMapsSection = "MultiMaps"; private const string GameModesSection = "GameModes"; private const string GameModeAliasesSection = "GameModeAliases"; private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { IncludeFields = true }; private MapFileWatcher mapFileWatcher; private readonly object mapModificationLock = new object(); private const int _mapChangeRetryCount = 3; private readonly List _gameModes = []; /// /// List of game modes. /// public IReadOnlyList GameModes => _gameModes; private GameModeMapCollection _gameModeMaps; public IReadOnlyGameModeMapCollection GameModeMaps => _gameModeMaps; /// /// An event that is fired when the maps have been loaded. /// public event EventHandler MapLoadingComplete; /// /// Fired when a map file is added, updated, or removed. /// public event EventHandler MapChanged; /// /// A list of game mode aliases. /// Every game mode entry that exists in this dictionary will get /// replaced by the game mode entries of the value string array /// when map is added to game mode map lists. /// private Dictionary GameModeAliases = new Dictionary(); private Dictionary _translatedMapNames = new(); /// /// A dictionary of translated map names. Used to look up the /// translated name of a map without knowing the ID of the map. /// public IReadOnlyDictionary TranslatedMapNames => _translatedMapNames; /// /// List of gamemodes allowed to be used on custom maps in order for them to display in map list. /// private string[] AllowedGameModes = ClientConfiguration.Instance.AllowedCustomGameModes.Split(','); public const int MapPreviewCacheCapacity = 100; private readonly IMapPreviewCacheManager mapPreviewCacheManager = new MapPreviewCacheManager(capacity: MapPreviewCacheCapacity); public MapLoader() { } public void Initialize() { MapLoadingComplete += (sender, args) => StartMapFileWatcher(); } /// /// Sets up file watching for maps. /// public void StartMapFileWatcher() { if (mapFileWatcher != null) return; string customMapsPath = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY); mapFileWatcher = new MapFileWatcher(customMapsPath, ClientConfiguration.Instance.MapFileExtension); mapFileWatcher.MapFileChanged += OnMapFileChanged; mapFileWatcher.StartWatching(); } /// /// Asynchronously loads maps based on INI info as well as those in the custom maps directory. /// public Task LoadMapsAsync() => Task.Run(LoadMapsInternalAsync); private async Task LoadMapsInternalAsync() { Logger.Log("MapLoader: Map loading task started."); var stopwatch = Stopwatch.StartNew(); string mpMapsPath = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath); Logger.Log($"MapLoader: Loading maps from {mpMapsPath}."); IniFile mpMapsIni = new IniFile(mpMapsPath); LoadGameModes(mpMapsIni); LoadGameModeAliases(mpMapsIni); // LoadMultiMapsAsync and LoadCustomMapsAsync both modify the game mode map collection. We intend to keep the collection non-thread-safe for performance, so the two methods must not be called simultaneously. await LoadMultiMapsAsync(mpMapsIni); await LoadCustomMapsAsync(); Logger.Log("MapLoader: Post-processing game mode map collections."); _gameModes.RemoveAll(g => g.Maps.Count < 1); _gameModeMaps = new GameModeMapCollection(_gameModes); // Clean up any name-based favorite entries after migration (legacy: changed from name to sha1) CleanupMigratedFavorites(); stopwatch.Stop(); Logger.Log($"MapLoader: Map loading complete. Total time: {stopwatch.ElapsedMilliseconds} ms"); MapLoadingComplete?.Invoke(this, EventArgs.Empty); } private async void OnMapFileChanged(object sender, MapFileEventArgs e) { switch (e.ChangeType) { case WatcherChangeTypes.Created: await HandleMapFileAdded(e.FilePath); break; case WatcherChangeTypes.Changed: await HandleMapFileChanged(e.FilePath); break; case WatcherChangeTypes.Deleted: await HandleMapFileDeleted(e.FilePath); break; } } private async Task HandleMapFileAdded(string filePath) { try { if (!File.Exists(filePath)) return; string baseFilePath = GetBaseFilePathFromFullPath(filePath); if (string.IsNullOrEmpty(baseFilePath)) return; // If, for instance, the file was just extracted, the program that created it may still // have a lock on the file. Retry a couple of times. Map map = null; bool success = false; for (int attempt = 0; attempt < _mapChangeRetryCount; attempt++) { try { map = new Map(baseFilePath, true); if (map.InitializeFromCustomMap()) { success = true; break; } } catch (IOException) { if (attempt < _mapChangeRetryCount - 1) await Task.Delay(100); else throw; } } if (success && map != null) { lock (mapModificationLock) { if (IsMapAlreadyLoaded(map.SHA1)) return; AddMapToGameModes(map, true); UpdateGameModeMaps(); Logger.Log($"MapLoader: Added new map {map.Name} from {filePath}"); MapChanged?.Invoke(this, new MapChangedEventArgs(map, MapChangeType.Added)); } } else { Logger.Log($"MapLoader: Failed to load map info from {filePath}"); } } catch (Exception ex) { Logger.Log($"MapLoader: Error adding map from {filePath}: {ex.Message}"); } } private async Task HandleMapFileChanged(string filePath) { try { string baseFilePath = GetBaseFilePathFromFullPath(filePath); if (string.IsNullOrEmpty(baseFilePath)) return; // If editing a map, the program that saved the new version may still // have a lock on the file. Retry a couple of times. Map newMap = null; bool success = false; for (int attempt = 0; attempt < _mapChangeRetryCount; attempt++) { try { newMap = new Map(baseFilePath, true); if (newMap.InitializeFromCustomMap()) { success = true; break; } } catch (IOException) { if (attempt < _mapChangeRetryCount - 1) await Task.Delay(100); else throw; } } if (success && newMap != null) { lock (mapModificationLock) { string oldSHA1 = FindMapSHA1ByFilePath(baseFilePath); if (!string.IsNullOrEmpty(oldSHA1)) { if (oldSHA1 != newMap.SHA1) { // SHA1 changed, remove old and add new RemoveMapBySHA1(oldSHA1); AddMapToGameModes(newMap, true); UpdateGameModeMaps(); Logger.Log($"MapLoader: Updated map {newMap.Name} from {filePath} (SHA1 changed: {oldSHA1} -> {newMap.SHA1})"); MapChanged?.Invoke(this, new MapChangedEventArgs(newMap, MapChangeType.Updated, oldSHA1)); } else { Logger.Log($"MapLoader: Map file {filePath} changed but SHA1 remained the same ({newMap.SHA1})"); } } else { // Map not found, treat as new Logger.Log($"MapLoader: Changed event for unknown map {filePath}, treating as new"); AddMapToGameModes(newMap, true); UpdateGameModeMaps(); MapChanged?.Invoke(this, new MapChangedEventArgs(newMap, MapChangeType.Added)); } } } else { Logger.Log($"MapLoader: Failed to reload map info from {filePath}"); } } catch (Exception ex) { Logger.Log($"MapLoader: Error updating map from {filePath}: {ex.Message}"); } } private async Task HandleMapFileDeleted(string filePath) { try { string baseFilePath = GetBaseFilePathFromFullPath(filePath); if (string.IsNullOrEmpty(baseFilePath)) return; lock (mapModificationLock) { string mapSHA1 = FindMapSHA1ByFilePath(baseFilePath); if (!string.IsNullOrEmpty(mapSHA1)) { var removedMap = FindMapBySHA1(mapSHA1); RemoveMapBySHA1(mapSHA1); UpdateGameModeMaps(); Logger.Log($"MapLoader: Removed map from {filePath}"); if (removedMap != null) MapChanged?.Invoke(this, new MapChangedEventArgs(removedMap, MapChangeType.Removed)); } } } catch (Exception ex) { Logger.Log($"MapLoader: Error removing map from {filePath}: {ex.Message}"); } } /// /// Converts a full file path to the base file path used by the map system. /// C:\YR\Maps\Custom\abc123.map > Maps\Custom\abc123 /// private string GetBaseFilePathFromFullPath(string fullPath) { try { string gamePathNormalized = Path.GetFullPath(ProgramConstants.GamePath); string fullPathNormalized = Path.GetFullPath(fullPath); if (!fullPathNormalized.StartsWith(gamePathNormalized, StringComparison.OrdinalIgnoreCase)) return null; string relativePath = fullPathNormalized.Substring(gamePathNormalized.Length); if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString()) || relativePath.StartsWith(Path.AltDirectorySeparatorChar.ToString())) { relativePath = relativePath.Substring(1); } string baseFilePath = relativePath.Substring(0, relativePath.Length - Path.GetExtension(relativePath).Length); return baseFilePath.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/'); } catch (Exception ex) { Logger.Log($"MapLoader: Error converting file path {fullPath}: {ex.Message}"); return null; } } private bool IsMapAlreadyLoaded(string sha1) => GameModes.SelectMany(gm => gm.Maps).Any(map => map.SHA1 == sha1); private Map FindMapBySHA1(string sha1) => GameModes.SelectMany(gm => gm.Maps).FirstOrDefault(map => map.SHA1 == sha1); private string FindMapSHA1ByFilePath(string baseFilePath) => GameModes.SelectMany(gm => gm.Maps) .Where(map => !map.Official && map.BaseFilePath.Equals(baseFilePath, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault()?.SHA1; private void RemoveMapBySHA1(string sha1) { foreach (var gameMode in GameModes) gameMode.Maps.RemoveAll(map => map.SHA1 == sha1); } private void UpdateGameModeMaps() { _gameModes.RemoveAll(g => g.Maps.Count < 1); _gameModeMaps = new GameModeMapCollection(_gameModes); } private async Task LoadMultiMapsAsync(IniFile mpMapsIni) { List keys = mpMapsIni.GetSectionKeys(MultiMapsSection); if (keys == null) { Logger.Log("Loading multiplayer map list failed!!!"); return; } Task[] tasks = keys.Select(key => Task.Run(() => { try { string mapFilePathValue = mpMapsIni.GetStringValue(MultiMapsSection, key, string.Empty); string mapFilePath = SafePath.CombineFilePath(mapFilePathValue); FileInfo mapFile = SafePath.GetFile(ProgramConstants.GamePath, FormattableString.Invariant($"{mapFilePath}.{ClientConfiguration.Instance.MapFileExtension}")); if (!mapFile.Exists) { Logger.Log("Map " + mapFile.FullName + " doesn't exist!"); return null; } var map = new Map(mapFilePathValue, false); if (!map.InitializeFromMpMapsINI(mpMapsIni)) return null; return map; } catch (Exception ex) { Logger.Log($"Error loading map for key {key}: {ex}"); return null; } })).ToArray(); Task waitMultiMapsTask = Task.WhenAll(tasks); while (await Task.WhenAny(waitMultiMapsTask, Task.Delay(1000)) != waitMultiMapsTask) { string message = "MapLoader: Waiting for the multiplayer map loading task to complete. Remaining files: " + tasks.Count(t => !t.IsCompleted) + ". Total: " + tasks.Length; Debug.WriteLine(message); Logger.Log(message); } await waitMultiMapsTask; foreach (Map map in tasks.Select(t => t.Result).Where(m => m != null)) { AddMapToGameModes(map, false); _translatedMapNames[map.UntranslatedName] = map.Name; } } private void LoadGameModes(IniFile mpMapsIni) { var gameModes = mpMapsIni.GetSectionKeys(GameModesSection); if (gameModes != null) { foreach (string key in gameModes) { string gameModeName = mpMapsIni.GetStringValue(GameModesSection, key, string.Empty); if (!string.IsNullOrEmpty(gameModeName)) { GameMode gm = new GameMode(gameModeName); _gameModes.Add(gm); } } } } private void LoadGameModeAliases(IniFile mpMapsIni) { var gmAliases = mpMapsIni.GetSectionKeys(GameModeAliasesSection); if (gmAliases != null) { foreach (string key in gmAliases) { GameModeAliases.Add(key, mpMapsIni.GetStringValue(GameModeAliasesSection, key, string.Empty).Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)); } } } private async Task LoadCustomMapsAsync() { DirectoryInfo customMapsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY); if (!customMapsDirectory.Exists) { Logger.Log($"Custom maps directory {customMapsDirectory} does not exist!"); return; } Logger.Log("MapLoader: Loading custom maps..."); // Load custom map cache from file system Stopwatch stopwatch = Stopwatch.StartNew(); IEnumerable mapFiles = customMapsDirectory.EnumerateFiles($"*.{ClientConfiguration.Instance.MapFileExtension}"); // Note: using synchronous file I/O here saves a noticeable amount of latency compared to async. CustomMapCache customMapCache = LoadCustomMapCache(); stopwatch.Stop(); Logger.Log(FormattableString.Invariant($"MapLoader: Loaded custom map cache from file system in {stopwatch.ElapsedMilliseconds} ms")); // Process uncached custom maps. stopwatch.Restart(); List localMapPaths; { int mapFileExtensionWithDotLength = $".{ClientConfiguration.Instance.MapFileExtension}".Length; Task[] tasks = mapFiles.Select(mapFile => Task.Run(() => { string baseFilePath = mapFile.FullName.Substring(ProgramConstants.GamePath.Length); baseFilePath = baseFilePath.Substring(0, baseFilePath.Length - mapFileExtensionWithDotLength); string normalizedPath = baseFilePath .Replace(Path.DirectorySeparatorChar, '/') .Replace(Path.AltDirectorySeparatorChar, '/'); if (customMapCache.Items.TryGetValue(normalizedPath, out var cachedItem) && !cachedItem.IsOutdated()) { // Use cached map return normalizedPath; } // Not in cache or outdated var map = new Map(normalizedPath, true); if (map.InitializeFromCustomMap()) customMapCache.Items[normalizedPath] = new CustomMapCache.Item(map); return normalizedPath; })).ToArray(); Task waitCustomMapsTask = Task.WhenAll(tasks); while (await Task.WhenAny(waitCustomMapsTask, Task.Delay(1000)) != waitCustomMapsTask) { string message = "MapLoader: Waiting for the custom map loading task to complete. Remaining files: " + tasks.Count(t => !t.IsCompleted) + ". Total: " + tasks.Length; Debug.WriteLine(message); Logger.Log(message); } await waitCustomMapsTask; localMapPaths = tasks.Select(t => t.Result).ToList(); } stopwatch.Stop(); Logger.Log(FormattableString.Invariant($"MapLoader: Processed uncached custom maps in {stopwatch.ElapsedMilliseconds} ms")); // Remove cached maps that no longer exist locally stopwatch.Restart(); HashSet missingMapPaths; { HashSet cachedMapPaths = customMapCache.Items.Keys.ToHashSet(); cachedMapPaths.ExceptWith(localMapPaths); missingMapPaths = cachedMapPaths; } foreach (string missingPath in missingMapPaths) customMapCache.Items.TryRemove(missingPath, out _); stopwatch.Stop(); Logger.Log(FormattableString.Invariant($"MapLoader: Removed outdated maps from cache in {stopwatch.ElapsedMilliseconds} ms")); // Save custom map cache stopwatch.Restart(); CacheCustomMaps(customMapCache); stopwatch.Stop(); Logger.Log(FormattableString.Invariant($"MapLoader: Saved custom map cache to disk in {stopwatch.ElapsedMilliseconds} ms")); foreach (Map map in customMapCache.Items.Values.Select(item => item.Map)) { AddMapToGameModes(map, false); } Logger.Log("MapLoader: Custom maps loaded."); } /// /// Save cache of custom maps. /// /// Custom maps to cache private void CacheCustomMaps(CustomMapCache customMapCache) { var jsonData = JsonSerializer.Serialize(customMapCache, jsonSerializerOptions); File.WriteAllText(CUSTOM_MAPS_CACHE, jsonData); } /// /// Load previously cached custom maps /// /// private CustomMapCache LoadCustomMapCache() { // Delete any legacy cache files foreach (string legacyCacheFile in LEGACY_CUSTOM_MAP_CACHE_FILES.Where(File.Exists)) { try { File.Delete(legacyCacheFile); } catch (Exception ex) { Logger.Log($"Failed to delete legacy custom map cache file {legacyCacheFile}: {ex.Message}"); } } // Load current cache try { var jsonData = File.ReadAllText(CUSTOM_MAPS_CACHE); var customMapCache = JsonSerializer.Deserialize(jsonData, jsonSerializerOptions); if (customMapCache?.Version != CurrentCustomMapCacheVersion) return new CustomMapCache() { Version = CurrentCustomMapCacheVersion, Items = [] }; foreach (CustomMapCache.Item customMap in customMapCache.Items.Values) customMap.Map.AfterDeserialize(recalculateSHA: false); // Remove outdated items foreach (var mapPath in customMapCache.Items.Keys.ToList()) { if (customMapCache.Items[mapPath].IsOutdated()) { customMapCache.Items.TryRemove(mapPath, out _); } } return customMapCache; } catch (Exception) { return new CustomMapCache() { Version = CurrentCustomMapCacheVersion, Items = [] }; } } /// /// Attempts to load a custom map. /// /// The path to the map file relative to the game directory. /// When method returns, contains a message reporting whether or not loading the map failed and how. /// The map if loading it was successful, otherwise false. public Map LoadCustomMap(string mapPath, out string resultMessage) { Debug.Assert(!mapPath.EndsWith($".{ClientConfiguration.Instance.MapFileExtension}", StringComparison.InvariantCultureIgnoreCase), $"Unexpected map path {mapPath}. It should not end with the map extension."); if (mapPath != mapPath.ToWin32FileName()) { Logger.Log("LoadCustomMap: Map " + FormattableString.Invariant($"{mapPath}.{ClientConfiguration.Instance.MapFileExtension}") + " contains WIN32API reserved characters!"); // Return "map file does not exist" message to hide technical details towards users resultMessage = string.Format("Map file {0} doesn't exist!".L10N("Client:MapLoader:MapFileDoesNotExist"), FormattableString.Invariant($"{mapPath}.{ClientConfiguration.Instance.MapFileExtension}")); return null; } string customMapFilePath = SafePath.CombineFilePath(ProgramConstants.GamePath, FormattableString.Invariant($"{mapPath}.{ClientConfiguration.Instance.MapFileExtension}")); FileInfo customMapFile = SafePath.GetFile(customMapFilePath); if (!customMapFile.Exists) { Logger.Log("LoadCustomMap: Map " + customMapFile.FullName + " not found!"); resultMessage = string.Format("Map file {0} doesn't exist!".L10N("Client:MapLoader:MapFileDoesNotExist"), customMapFile.Name); return null; } Logger.Log("LoadCustomMap: Loading custom map " + customMapFile.FullName); var map = new Map(mapPath, true); if (map.InitializeFromCustomMap()) { foreach (GameMode gm in GameModes) { if (gm.Maps.Find(m => m.SHA1 == map.SHA1) != null) { Logger.Log("LoadCustomMap: Custom map " + customMapFile.FullName + " is already loaded!"); resultMessage = string.Format("Map {0} is already loaded.".L10N("Client:MapLoader:MapAlreadyLoaded"), map.Name); return null; } } Logger.Log("LoadCustomMap: Map " + customMapFile.FullName + " added successfully."); AddMapToGameModes(map, true); var gameModes = GameModes.Where(gm => gm.Maps.Contains(map)); _gameModeMaps.AddRange(gameModes.Select(gm => new GameModeMap(gm, map, false))); resultMessage = string.Format("Map {0} loaded successfully.".L10N("Client:MapLoader:MapLoadedSuccessfully"), map.Name); return map; } Logger.Log("LoadCustomMap: Loading map " + customMapFile.FullName + " failed!"); resultMessage = string.Format("Loading map {0} failed!".L10N("Client:MapLoader:MapLoadingFailed"), Path.GetFileNameWithoutExtension(customMapFile.Name)); return null; } public void DeleteCustomMap(GameModeMap gameModeMap) { Logger.Log("Deleting map " + gameModeMap.Map.UntranslatedName); File.Delete(gameModeMap.Map.CompleteFilePath); foreach (GameMode gameMode in GameModeMaps.GameModes) { gameMode.Maps.Remove(gameModeMap.Map); } _gameModeMaps.Remove(gameModeMap); } /// /// Adds map to all eligible game modes. /// /// Map to add. /// If set to true, a message for each game mode the map is added to is output to the log file. private void AddMapToGameModes(Map map, bool enableLogging) { foreach (string gameMode in map.GameModes) { if (!GameModeAliases.TryGetValue(gameMode, out string[] gameModeAliases)) gameModeAliases = new string[] { gameMode }; foreach (string gameModeAlias in gameModeAliases) { if (!map.Official && !(AllowedGameModes.Contains(gameMode) || AllowedGameModes.Contains(gameModeAlias))) continue; GameMode gm = GameModes.FirstOrDefault(g => g.Name == gameModeAlias); if (gm == null) { gm = new GameMode(gameModeAlias); _gameModes.Add(gm); } gm.Maps.Add(map); if (enableLogging) Logger.Log("AddMapToGameModes: Added map " + map.UntranslatedName + " to game mode " + gm.Name); } } } /// /// Removes any name-based favorite entries that have been successfully migrated to SHA1. /// This runs after all maps have been processed to ensure complete migration. /// private void CleanupMigratedFavorites() { var favoriteMaps = UserINISettings.Instance.FavoriteMaps; if (favoriteMaps == null || !favoriteMaps.Any()) return; var entriesToRemove = new List(); foreach (string favoriteKey in favoriteMaps) { string[] parts = favoriteKey.Split(':'); if (parts.Length != 2) continue; string mapName = parts[0]; string gameModeName = parts[1]; // Check if there's a corresponding SHA1-based entry for any map with this name var gameMode = GameModes.FirstOrDefault(gm => gm.Name == gameModeName); if (gameMode != null) { bool hasMigratedVersion = gameMode.Maps .Where(m => m.UntranslatedName == mapName) .Any(m => favoriteMaps.Contains($"{m.SHA1}:{gameModeName}")); if (hasMigratedVersion) entriesToRemove.Add(favoriteKey); } } // Remove the name-based entries if (entriesToRemove.Any()) { foreach (string entry in entriesToRemove) favoriteMaps.Remove(entry); UserINISettings.Instance.WriteFavoriteMaps(); } } public void PrefetchCachedPreviewImageFromMap(Map map) { if (map?.IsNonImmediatePreviewImageAvailable() ?? false) _ = mapPreviewCacheManager.Request(map, out Image _, addToQueue: true); } public Image GetCachedPreviewImageFromMap(Map map, bool syncLoadOnCacheMiss = false) { if (map?.IsImmediatePreviewImageAvailable() ?? false) { return map.GetImmediatePreviewImage(); } else if (map?.IsNonImmediatePreviewImageAvailable() ?? false) { if (mapPreviewCacheManager.Request(map, out Image image, syncComputeOnCacheMiss: syncLoadOnCacheMiss, addToQueue: true)) return image; else return null; } else { return null; } } public Map FindMapByHash(string mapHash) => GameModeMaps?.FindMapByHash(mapHash); public void Dispose() => mapPreviewCacheManager?.Dispose(); } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MapPreviewCacheManager.cs ================================================ #nullable enable using SixLabors.ImageSharp; namespace DTAClient.Domain.Multiplayer; /// /// Thread-safe manager for caching map preview images with LRU eviction policy. /// Processes image extraction requests sequentially to limit CPU usage to a single thread. /// Note: this manager assumes the `Image` objects are managed, so it never disposes them directly. /// public class MapPreviewCacheManager : CacheManagerBase, IMapPreviewCacheManager { public MapPreviewCacheManager(int capacity) : base(capacity) { } public override string Name => nameof(MapPreviewCacheManager); protected override Image? ComputeOutputForInput(Map map) { if (!map.IsNonImmediatePreviewImageAvailable()) return null; Image? image = map.GetNonImmediatePreviewImage(); return image; } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs ================================================ using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text; using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; using lzo.net; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace DTAClient.Domain.Multiplayer { /// /// A helper class for extracting preview images from maps. /// public static class MapPreviewExtractor { /// /// Extracts map preview image as a bitmap. /// /// Map file. /// Bitmap of map preview image, or null if preview could not be extracted. public static Image ExtractMapPreview(IniFile mapIni) { List sectionKeys = mapIni.GetSectionKeys("PreviewPack"); string baseFilename = mapIni.FileName.Replace(ProgramConstants.GamePath, ""); if (sectionKeys == null || sectionKeys.Count == 0) { Logger.Log("MapPreviewExtractor: " + baseFilename + " - no [PreviewPack] exists, unable to extract preview."); return null; } if (mapIni.GetStringValue("PreviewPack", "1", string.Empty) == "yAsAIAXQ5PDQ5PDQ6JQATAEE6PDQ4PDI4JgBTAFEAkgAJyAATAG0AydEAEABpAJIA0wBVA") { Logger.Log("MapPreviewExtractor: " + baseFilename + " - Hidden preview detected, not extracting preview."); return null; } string[] previewSizes = mapIni.GetStringListValue("Preview", "Size", string.Empty); int previewWidth = previewSizes.Length > 3 ? Conversions.IntFromString(previewSizes[2], -1) : -1; int previewHeight = previewSizes.Length > 3 ? Conversions.IntFromString(previewSizes[3], -1) : -1; if (previewWidth < 1 || previewHeight < 1) { Logger.Log("MapPreviewExtractor: " + baseFilename + " - [Preview] Size value is invalid, unable to extract preview."); return null; } StringBuilder sb = new StringBuilder(); if (sectionKeys != null) { foreach (string key in sectionKeys) sb.Append(mapIni.GetStringValue("PreviewPack", key, string.Empty)); } byte[] dataSource; try { dataSource = Convert.FromBase64String(sb.ToString()); } catch (Exception) { Logger.Log("MapPreviewExtractor: " + baseFilename + " - [PreviewPack] is malformed, unable to extract preview."); return null; } byte[] dataDest = DecompressPreviewData(dataSource, previewWidth * previewHeight * 3, out string errorMessage); if (errorMessage != null) { Logger.Log("MapPreviewExtractor: " + baseFilename + " - " + errorMessage); return null; } Image bitmap = CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest, out errorMessage); if (errorMessage != null) { Logger.Log("MapPreviewExtractor: " + baseFilename + " - " + errorMessage); return null; } return bitmap; } /// /// Decompresses map preview image data. /// /// Array of compressed map preview image data. /// Size of decompressed preview image data. /// Will be set to error message if something went wrong, otherwise null. /// Array of decompressed preview image data if successfully decompressed, otherwise null. private static byte[] DecompressPreviewData(byte[] dataSource, int decompressedDataSize, out string errorMessage) { try { byte[] dataDest = new byte[decompressedDataSize]; int readBytes = 0, writtenBytes = 0; while (true) { if (readBytes >= dataSource.Length) break; ushort sizeCompressed = BinaryPrimitives.ReadUInt16LittleEndian(dataSource.AsSpan(readBytes)); readBytes += 2; ushort sizeUncompressed = BinaryPrimitives.ReadUInt16LittleEndian(dataSource.AsSpan(readBytes)); readBytes += 2; if (sizeCompressed == 0 || sizeUncompressed == 0) break; if (readBytes + sizeCompressed > dataSource.Length || writtenBytes + sizeUncompressed > dataDest.Length) { errorMessage = "Preview data does not match preview size or the data is corrupted, unable to extract preview."; return null; } LzoStream stream = new LzoStream(new MemoryStream(dataSource, readBytes, sizeCompressed), CompressionMode.Decompress); stream.Read(dataDest, writtenBytes, sizeUncompressed); readBytes += sizeCompressed; writtenBytes += sizeUncompressed; } errorMessage = null; return dataDest; } catch (Exception ex) { errorMessage = "Error encountered decompressing preview data. Message: " + ex.Message; return null; } } /// /// Creates a preview bitmap based on a provided dimensions and raw image pixel data in 24-bit RGB format. /// /// Width of the bitmap. /// Height of the bitmap. /// Raw image pixel data in 24-bit RGB format. /// Will be set to error message if something went wrong, otherwise null. /// Bitmap based on the provided dimensions and raw image data, or null if length of image data does not match the provided dimensions or if something went wrong. private static Image CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData, out string errorMessage) { const int pixelFormatBitCount = 24; const int pixelFormatByteCount = pixelFormatBitCount / 8; if (imageData.Length != width * height * pixelFormatByteCount) { errorMessage = "Provided preview image dimensions do not match preview image data length."; return null; } try { int strideWidth = (((width * pixelFormatBitCount) + 31) & ~31) >> 3; int numSkipBytes = strideWidth - (width * pixelFormatByteCount); byte[] bitmapPixelData = new byte[strideWidth * height]; int writtenBytes = 0; int readBytes = 0; for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { // GDI+ bitmap raw pixel data is in BGR format, red & blue values need to be flipped around for each pixel. bitmapPixelData[writtenBytes] = imageData[readBytes + 2]; bitmapPixelData[writtenBytes + 1] = imageData[readBytes + 1]; bitmapPixelData[writtenBytes + 2] = imageData[readBytes]; writtenBytes += pixelFormatByteCount; readBytes += pixelFormatByteCount; } // GDI+ bitmap stride / scan width has to be a multiple of 4, so the end of each stride / scanline can contain extra bytes // in the bitmap raw pixel data that are not present in the image data and should be skipped when copying. writtenBytes += numSkipBytes; } // https://github.com/SixLabors/ImageSharp/blob/main/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs var image = new Image(width, height); Configuration configuration = image.GetConfiguration(); Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(width); unsafe { fixed (byte* sourcePtrBase = &bitmapPixelData[0]) { fixed (Bgr24* destPtr = &workBuffer.Memory.Span[0]) { for (int rowCount = 0; rowCount < height; rowCount++) { Span row = imageBuffer.DangerousGetRowSpan(rowCount); byte* sourcePtr = sourcePtrBase + (strideWidth * rowCount); Buffer.MemoryCopy(sourcePtr, destPtr, strideWidth, strideWidth); PixelOperations.Instance.FromBgr24(configuration, workBuffer.Memory.Span[..width], row); } } } } errorMessage = null; return image; } catch (Exception ex) { errorMessage = "Error encountered creating preview bitmap. Message: " + ex.Message; return null; } } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/MultiplayerColor.cs ================================================ using ClientCore; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using System; using System.Collections.Generic; namespace DTAClient.Domain.Multiplayer { /// /// A color for the multiplayer game lobby. /// public class MultiplayerColor { public int GameColorIndex { get; private set; } public string Name { get; private set; } public Color XnaColor { get; private set; } private static List colorList; /// /// Creates a new multiplayer color from data in a string array. /// /// The name of the color. /// The input data. Needs to be in the format R,G,B,(game color index). /// A new multiplayer color created from the given string array. public static MultiplayerColor CreateFromStringArray(string name, string[] data) { return new MultiplayerColor() { Name = name, XnaColor = new Color(Math.Min(255, Int32.Parse(data[0])), Math.Min(255, Int32.Parse(data[1])), Math.Min(255, Int32.Parse(data[2])), 255), GameColorIndex = Int32.Parse(data[3]) }; } /// /// Returns the available multiplayer colors. /// public static List LoadColors() { if (colorList != null) return new List(colorList); IniFile gameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "GameOptions.ini")); List mpColors = new List(); List colorKeys = gameOptionsIni.GetSectionKeys("MPColors"); if (colorKeys == null) throw new ClientConfigurationException("[MPColors] not found in GameOptions.ini!"); foreach (string key in colorKeys) { string[] values = gameOptionsIni.GetStringListValue("MPColors", key, "255,255,255,0"); try { MultiplayerColor mpColor = MultiplayerColor.CreateFromStringArray(key.L10N($"INI:Colors:{key}"), values); mpColors.Add(mpColor); } catch { throw new ClientConfigurationException("Invalid MPColor specified in GameOptions.ini: " + key); } } colorList = mpColors; return new List(colorList); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs ================================================ using ClientCore.Extensions; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DTAClient.Domain.Multiplayer { public class PlayerExtraOptions { private static string INVALID_OPTIONS_MESSAGE => "Invalid player extra options message".L10N("Client:Main:InvalidPlayerExtraOptionsMessage"); private static string MAPPING_ERROR_PREFIX => "Auto Allying:".L10N("Client:Main:AutoAllyingPrefix"); protected static string NOT_ALL_MAPPINGS_ASSIGNED => MAPPING_ERROR_PREFIX + " " + "You must have all mappings assigned.".L10N("Client:Main:NotAllMappingsAssigned"); protected static string MULTIPLE_MAPPINGS_ASSIGNED_TO_SAME_START => MAPPING_ERROR_PREFIX + " " + "Multiple mappings assigned to the same start location.".L10N("Client:Main:MultipleMappingsAssigned"); protected static string ONLY_ONE_TEAM => MAPPING_ERROR_PREFIX + " " + "You must have more than one team assigned.".L10N("Client:Main:OnlyOneTeam"); private const char MESSAGE_SEPARATOR = ';'; public const string CNCNET_MESSAGE_KEY = "PEO"; public const string LAN_MESSAGE_KEY = "PEOPTS"; public bool IsForceRandomSides { get; set; } public bool IsForceRandomColors { get; set; } public bool IsForceNoTeams { get; set; } public bool IsForceRandomStarts { get; set; } public bool IsUseTeamStartMappings { get; set; } public List TeamStartMappings { get; set; } = new List(); public string GetTeamMappingsError() { if (!IsUseTeamStartMappings) return null; var distinctStartLocations = TeamStartMappings.Select(m => m.Start).Distinct(); if (distinctStartLocations.Count() != TeamStartMappings.Count) return MULTIPLE_MAPPINGS_ASSIGNED_TO_SAME_START; // multiple mappings are using the same spawn location var distinctTeams = TeamStartMappings.Select(m => m.Team).Distinct(); if (distinctTeams.Count() < 2) return ONLY_ONE_TEAM; // must have more than one team assigned return null; } public string ToCncnetMessage() => $"{CNCNET_MESSAGE_KEY} {ToString()}"; public string ToLanMessage() => $"{LAN_MESSAGE_KEY} {ToString()}"; public override string ToString() { var stringBuilder = new StringBuilder(); stringBuilder.Append(IsForceRandomSides ? "1" : "0"); stringBuilder.Append(IsForceRandomColors ? "1" : "0"); stringBuilder.Append(IsForceNoTeams ? "1" : "0"); stringBuilder.Append(IsForceRandomStarts ? "1" : "0"); stringBuilder.Append(IsUseTeamStartMappings ? "1" : "0"); stringBuilder.Append(MESSAGE_SEPARATOR); stringBuilder.Append(TeamStartMapping.ToListString(TeamStartMappings)); return stringBuilder.ToString(); } public static PlayerExtraOptions FromMessage(string message) { var parts = message.Split(MESSAGE_SEPARATOR); if (parts.Length < 2) throw new Exception(INVALID_OPTIONS_MESSAGE); var boolParts = parts[0].ToCharArray(); if (boolParts.Length < 5) throw new Exception(INVALID_OPTIONS_MESSAGE); return new PlayerExtraOptions { IsForceRandomSides = boolParts[0] == '1', IsForceRandomColors = boolParts[1] == '1', IsForceNoTeams = boolParts[2] == '1', IsForceRandomStarts = boolParts[3] == '1', IsUseTeamStartMappings = boolParts[4] == '1', TeamStartMappings = TeamStartMapping.FromListString(parts[1]) }; } public bool IsDefault() { var defaultPLayerExtraOptions = new PlayerExtraOptions(); return IsForceRandomColors == defaultPLayerExtraOptions.IsForceRandomColors && IsForceRandomStarts == defaultPLayerExtraOptions.IsForceRandomStarts && IsForceNoTeams == defaultPLayerExtraOptions.IsForceNoTeams && IsForceRandomSides == defaultPLayerExtraOptions.IsForceRandomSides && IsUseTeamStartMappings == defaultPLayerExtraOptions.IsUseTeamStartMappings; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/PlayerHouseInfo.cs ================================================ using System; using System.Collections.Generic; using ClientCore; namespace DTAClient.Domain.Multiplayer { public class PlayerHouseInfo { public int SideIndex { get; set; } /// /// A side (or, more correctly, house or country depending on the game) /// index that is used in rules file of the game. /// public int InternalSideIndex { get { if (IsSpectator && !string.IsNullOrEmpty(ClientConfiguration.Instance.SpectatorInternalSideIndex)) return int.Parse(ClientConfiguration.Instance.SpectatorInternalSideIndex); if (!string.IsNullOrEmpty(ClientConfiguration.Instance.InternalSideIndices)) return Array.ConvertAll(ClientConfiguration.Instance.InternalSideIndices.Split(','), int.Parse)[SideIndex]; return SideIndex; } } public int ColorIndex { get; set; } public int StartingWaypoint { get; set; } public int RealStartingWaypoint { get; set; } public bool IsSpectator { get; set; } /// /// Applies the player's side into the information /// and randomizes it if necessary. /// /// The PlayerInfo of the player. /// The number of sides in the game. /// Random number generator. /// A bool array that determines which side indexes are disallowed by game options. public void RandomizeSide(PlayerInfo pInfo, int sideCount, Random random, bool[] disallowedSideArray, List randomSelectors, int randomCount) { if (pInfo.SideId == 0 || pInfo.SideId == sideCount + randomCount) { // The player has selected Random or Spectator int sideId; do sideId = random.Next(0, sideCount); while (disallowedSideArray[sideId]); SideIndex = sideId; } else { // Use custom random selector. if (pInfo.SideId < randomCount) { int[] randomsides = randomSelectors[pInfo.SideId - 1]; int count = randomsides.Length; int sideId; do sideId = randomsides[random.Next(0, count)]; while (disallowedSideArray[sideId]); SideIndex = sideId; } else SideIndex = pInfo.SideId - randomCount; // The player has selected a side } } /// /// Applies the player's color into the information and randomizes /// it if necessary. If the color is randomized, it's removed /// from the list of available colors. /// /// The PlayerInfo of the player. /// The list of available (un-used) colors. /// The list of all multiplayer colors. /// Random number generator. public void RandomizeColor(PlayerInfo pInfo, List freeColors, List mpColors, Random random) { if (pInfo.ColorId == 0) { // The player has selected Random for their color int randomizedColorIndex = random.Next(0, freeColors.Count); int actualColorId = freeColors[randomizedColorIndex]; ColorIndex = mpColors[actualColorId].GameColorIndex; freeColors.RemoveAt(randomizedColorIndex); } else { ColorIndex = mpColors[pInfo.ColorId - 1].GameColorIndex; freeColors.Remove(pInfo.ColorId - 1); } } /// /// Applies the player's starting location into the information and /// randomizes it if necessary. If the starting location is randomized, /// the starting location is removed from the list of available starting locations. /// /// The PlayerInfo of the player. /// List of free starting locations. /// Random number generator. /// A list of starting locations that are already occupied. /// /// True if the player's starting location index exceeds the map's number of starting waypoints, /// otherwise false. public void RandomizeStart( PlayerInfo pInfo, Random random, List freeStartingLocations, List takenStartingLocations, bool overrideGameRandomLocations ) { overrideGameRandomLocations |= ClientConfiguration.Instance.UseClientRandomStartLocations; if (IsSpectator) { StartingWaypoint = 90; return; } if (pInfo.StartingLocation == 0) { // Randomize starting location if (!overrideGameRandomLocations) { // The game uses its own randomization logic that places // randomized players on the opposite side of the map // Players seem to prefer this behaviour, so use -1 to // leave randomizing the starting location to the game itself RealStartingWaypoint = -1; StartingWaypoint = -1; return; } // Let the client pick starting positions. if (freeStartingLocations.Count == 0) // No free starting locs available { RealStartingWaypoint = -1; StartingWaypoint = -1; return; } int waypointIndex = random.Next(0, freeStartingLocations.Count); RealStartingWaypoint = freeStartingLocations[waypointIndex]; StartingWaypoint = RealStartingWaypoint; freeStartingLocations.Remove(StartingWaypoint); return; } // Use the player's selected starting location RealStartingWaypoint = pInfo.StartingLocation - 1; if (takenStartingLocations.Contains(RealStartingWaypoint)) { StartingWaypoint = -1; // Unknown starting location, stacked with another player return; } takenStartingLocations.Add(RealStartingWaypoint); StartingWaypoint = RealStartingWaypoint; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/PlayerInfo.cs ================================================ using Rampastring.Tools; using System; namespace DTAClient.Domain.Multiplayer { /// /// A player in the game lobby. /// public class PlayerInfo { public PlayerInfo() { } public PlayerInfo(string name) { Name = name; } public PlayerInfo(string name, int sideId, int startingLocation, int colorId, int teamId) { Name = name; SideId = sideId; StartingLocation = startingLocation; ColorId = colorId; TeamId = teamId; } public string Name { get; set; } public int SideId { get; set; } public int StartingLocation { get; set; } public int ColorId { get; set; } public int TeamId { get; set; } public bool Ready { get; set; } public bool AutoReady { get; set; } public bool IsAI { get; set; } public bool IsInGame { get; set; } public virtual string IPAddress { get; set; } = "0.0.0.0"; public int Port { get; set; } /// /// Whether the file hash information is received from the player, regardless of whether it is consistent with the one calculated by this client. /// public bool HashReceived { get; set; } public int Index { get; set; } public int Ping { get; set; } = -1; /// /// The difficulty level of an AI player for in-client purposes. /// Logical increasing scale, like in the vanilla Tiberian Sun UI. /// 2 = Hard, 1 = Medium, 0 = Easy. /// public int AILevel { get; set; } /// /// The AI level of the AI for the [HouseHandicaps] section in spawn.ini. /// 2 = Easy, 1 = Medium, 0 = Hard. /// public int HouseHandicapAILevel { get { return Math.Abs(AILevel - 2); } } public override string ToString() { var sb = new ExtendedStringBuilder(true, ','); sb.Append(Name); sb.Append(SideId); sb.Append(StartingLocation); sb.Append(ColorId); sb.Append(TeamId); sb.Append(AILevel); sb.Append(IsAI.ToString()); sb.Append(Index); return sb.ToString(); } /// /// Creates a PlayerInfo instance from a string in a format that matches the /// string given by the ToString() method. /// /// The string. /// A PlayerInfo instance, or null if the string format was invalid. public static PlayerInfo FromString(string str) { var values = str.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (values.Length != 8) return null; var pInfo = new PlayerInfo(); pInfo.Name = values[0]; pInfo.SideId = Conversions.IntFromString(values[1], 0); pInfo.StartingLocation = Conversions.IntFromString(values[2], 0); pInfo.ColorId = Conversions.IntFromString(values[3], 0); pInfo.TeamId = Conversions.IntFromString(values[4], 0); pInfo.AILevel = Conversions.IntFromString(values[5], 0); pInfo.IsAI = Conversions.BooleanFromString(values[6], true); pInfo.Index = Conversions.IntFromString(values[7], 0); return pInfo; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/SavedGamePlayer.cs ================================================ namespace DTAClient.Domain.Multiplayer { public class SavedGamePlayer { public string Name { get; set; } public int ColorIndex { get; set; } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/TeamStartMapping.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using ClientCore; namespace DTAClient.Domain.Multiplayer { public class TeamStartMapping { private const char LIST_SEPARATOR = ','; public const string NO_PLAYER = "x"; public const string NO_TEAM = "-"; public static readonly List TEAMS = new List() { NO_PLAYER, NO_TEAM }.Concat(ProgramConstants.TEAMS).ToList(); [JsonInclude] [JsonPropertyName("t")] public string Team { get; set; } [JsonInclude] [JsonPropertyName("s")] public int Start { get; set; } [JsonIgnore] public bool IsValid => TeamIndex != -1; [JsonIgnore] public int TeamIndex => TEAMS.IndexOf(Team); [JsonIgnore] public int TeamId => ProgramConstants.TEAMS.IndexOf(Team) + 1; [JsonIgnore] public int StartingWaypoint => Start - 1; [JsonIgnore] public bool IsBlock => Team == NO_PLAYER; /// /// Write these out in a delimited list. /// /// /// public static string ToListString(List teamStartMappings) => string.Join(LIST_SEPARATOR.ToString(), teamStartMappings.Select(mapping => mapping.Team)); /// /// This parses a list of classes that were written out as a list /// for either message purposes or a map INI. /// /// /// public static List FromListString(string str) { if (string.IsNullOrWhiteSpace(str)) return new List(); var parts = str.Split(LIST_SEPARATOR); return parts.Select((part, index) => new TeamStartMapping() { Team = part, Start = index + 1 }).ToList(); } } } ================================================ FILE: DXMainClient/Domain/Multiplayer/TeamStartMappingPreset.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; namespace DTAClient.Domain.Multiplayer { public class TeamStartMappingPreset { [JsonInclude] [JsonPropertyName("n")] public string Name { get; set; } [JsonInclude] [JsonPropertyName("m")] public List TeamStartMappings { get; set; } } } ================================================ FILE: DXMainClient/Domain/SavedGame.cs ================================================ using ClientCore; using Rampastring.Tools; using System; using System.Buffers.Binary; using System.IO; using OpenMcdf; using System.Diagnostics; namespace DTAClient.Domain { /// /// A single-player saved game. /// public class SavedGame { const string SAVED_GAME_PATH = "Saved Games/"; public SavedGame(string fileName) { FileName = fileName; } public string FileName { get; private set; } public string GUIName { get; private set; } public DateTime LastModified { get; private set; } public int CustomMissionID { get; private set; } /// /// Reads and sets the saved game's name and last modified date, and returns true if succesful. /// /// True if parsing the info was succesful, otherwise false. public bool ParseInfo() { try { FileInfo savedGameFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SAVED_GAME_PATH, FileName); using (Stream file = savedGameFileInfo.Open(FileMode.Open, FileAccess.Read)) { var cf = new CompoundFile(file); GUIName = System.Text.Encoding.Unicode.GetString(cf.RootStorage.GetStream("Scenario Description").GetData()).TrimEnd(['\0']); try { CustomMissionID = BinaryPrimitives.ReadInt32LittleEndian(cf.RootStorage.GetStream("CustomMissionID").GetData()); } catch (CFItemNotFound) { CustomMissionID = 0; } } LastModified = savedGameFileInfo.LastWriteTime; return true; } catch (Exception ex) { Logger.Log("An error occured while parsing saved game " + FileName + ":" + ex.ToString()); return false; } } } } ================================================ FILE: DXMainClient/Online/Channel.cs ================================================ using ClientCore; using ClientCore.Enums; using DTAClient.Online.EventArguments; using System; using System.Collections.Generic; using DTAClient.DXGUI; using ClientCore.Extensions; using System.Diagnostics; namespace DTAClient.Online { public class Channel : IMessageView { const int MESSAGE_LIMIT = 1024; public event EventHandler UserAdded; public event EventHandler UserLeft; public event EventHandler UserKicked; public event EventHandler UserQuitIRC; public event EventHandler UserGameIndexUpdated; public event EventHandler UserNameChanged; public event EventHandler UserListReceived; public event EventHandler UserListCleared; public event EventHandler MessageAdded; public event EventHandler ChannelModesChanged; public event EventHandler CTCPReceived; public event EventHandler InvalidPasswordEntered; public event EventHandler InviteOnlyErrorOnJoin; /// /// Raised when the server informs the client that it's is unable to /// join the channel because it's full. /// public event EventHandler ChannelFull; /// /// Raised when the server informs the client that it's is unable to /// join the channel because the client has attempted to join too many /// channels too quickly. /// public event EventHandler TargetChangeTooFast; public Channel(string uiName, string channelName, bool persistent, bool isChatChannel, string password, Connection connection) { if (isChatChannel) users = new SortedUserCollection(ChannelUser.ChannelUserComparison); else users = new UnsortedUserCollection(); UIName = uiName; ChannelName = channelName.ToLowerInvariant(); Persistent = persistent; IsChatChannel = isChatChannel; Password = password; this.connection = connection; if (persistent) { Instance_SettingsSaved(null, EventArgs.Empty); UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved; } } #region Public members public string UIName { get; set; } public string ChannelName { get; } public bool Persistent { get; } public bool IsChatChannel { get; } public string Password { get; private set; } private readonly Connection connection; string _topic; public string Topic { get { return _topic; } set { _topic = value; if (Persistent) AddMessage(new ChatMessage( string.Format("Topic for {0} is: {1}".L10N("Client:Main:ChannelTopic"), UIName, _topic))); } } List messages = new List(); public List Messages => messages; IUserCollection users; public IUserCollection Users => users; #endregion bool notifyOnUserListChange = true; private void Instance_SettingsSaved(object sender, EventArgs e) { if (ClientConfiguration.Instance.ClientGameType == ClientType.YR) notifyOnUserListChange = false; else notifyOnUserListChange = UserINISettings.Instance.NotifyOnUserListChange; } public void AddUser(ChannelUser user) { users.Add(user.IRCUser.Name, user); UserAdded?.Invoke(this, new ChannelUserEventArgs(user)); } public void OnUserJoined(ChannelUser user) { AddUser(user); if (notifyOnUserListChange) { AddMessage(new ChatMessage( string.Format("{0} has joined {1}.".L10N("Client:Main:PlayerJoinChannel"), user.IRCUser.Name, UIName))); } if (ClientConfiguration.Instance.ClientGameType != ClientType.YR) { if (Persistent && IsChatChannel && user.IRCUser.Name == ProgramConstants.PLAYERNAME) RequestUserInfo(); } } public void OnUserListReceived(List userList) { for (int i = 0; i < userList.Count; i++) { ChannelUser user = userList[i]; var existingUser = users.Find(user.IRCUser.Name); if (existingUser == null) { users.Add(user.IRCUser.Name, user); } else if (IsChatChannel) { if (existingUser.IsAdmin != user.IsAdmin) { existingUser.IsAdmin = user.IsAdmin; existingUser.IsFriend = user.IsFriend; // Note: IUserCollection.Reinsert() is not guaranteed to be implemented, unless it is a SortedUserCollection Debug.Assert(users is SortedUserCollection, "Channel 'users' is supposed to be a SortedUserCollection"); users.Reinsert(user.IRCUser.Name); } } } UserListReceived?.Invoke(this, EventArgs.Empty); } public void OnUserKicked(string userName) { if (users.Remove(userName)) { if (userName == ProgramConstants.PLAYERNAME) { users.Clear(); } AddMessage(new ChatMessage( string.Format("{0} has been kicked from {1}.".L10N("Client:Main:PlayerKickedFromChannel"), userName, UIName))); UserKicked?.Invoke(this, new UserNameEventArgs(userName)); } } public void OnUserLeft(string userName) { if (users.Remove(userName)) { if (notifyOnUserListChange) { AddMessage(new ChatMessage( string.Format("{0} has left from {1}.".L10N("Client:Main:PlayerLeftFromChannel"), userName, UIName))); } UserLeft?.Invoke(this, new UserNameEventArgs(userName)); } } public void OnUserQuitIRC(string userName) { if (users.Remove(userName)) { if (notifyOnUserListChange) { AddMessage(new ChatMessage( string.Format("{0} has quit from CnCNet.".L10N("Client:Main:PlayerQuitCncNet"), userName))); } UserQuitIRC?.Invoke(this, new UserNameEventArgs(userName)); } } public void UpdateGameIndexForUser(string userName) { var user = users.Find(userName); if (user != null) UserGameIndexUpdated?.Invoke(this, new ChannelUserEventArgs(user)); } public void OnUserNameChanged(string oldUserName, string newUserName) { var user = users.Find(oldUserName); if (user != null) { users.Remove(oldUserName); users.Add(newUserName, user); UserNameChanged?.Invoke(this, new UserNameChangedEventArgs(oldUserName, user.IRCUser)); } } public void OnChannelModesChanged(string sender, string modes) { ChannelModesChanged?.Invoke(this, new ChannelModeEventArgs(sender, modes)); } public void OnCTCPReceived(string userName, string message) { CTCPReceived?.Invoke(this, new ChannelCTCPEventArgs(userName, message)); } public void OnInvalidJoinPassword() { InvalidPasswordEntered?.Invoke(this, EventArgs.Empty); } public void OnInviteOnlyOnJoin() { InviteOnlyErrorOnJoin?.Invoke(this, EventArgs.Empty); } public void OnChannelFull() { ChannelFull?.Invoke(this, EventArgs.Empty); } public void OnTargetChangeTooFast(string message) { TargetChangeTooFast?.Invoke(this, new MessageEventArgs(message)); } public void AddMessage(ChatMessage message) { if (messages.Count == MESSAGE_LIMIT) messages.RemoveAt(0); messages.Add(message); MessageAdded?.Invoke(this, new IRCMessageEventArgs(message)); } public void SendChatMessage(string message, IRCColor color) { AddMessage(new ChatMessage(ProgramConstants.PLAYERNAME, color.XnaColor, DateTime.Now, message)); string colorString = ((char)03).ToString() + color.IrcColorId.ToString("D2"); connection.QueueMessage(QueuedMessageType.CHAT_MESSAGE, 0, "PRIVMSG " + ChannelName + " :" + colorString + message); } /// /// /// /// /// This can be used to help prevent flooding for multiple options that are changed quickly. It allows for a single message /// for multiple changes. /// public void SendCTCPMessage(string message, QueuedMessageType qmType, int priority, bool replace = false) { char CTCPChar1 = (char)58; char CTCPChar2 = (char)01; connection.QueueMessage(qmType, priority, "NOTICE " + ChannelName + " " + CTCPChar1 + CTCPChar2 + message + CTCPChar2, replace); } /// /// Sends a "kick user" message to the channel. /// /// The name of the user that should be kicked. /// The priority of the message in the send queue. public void SendKickMessage(string userName, int priority) { connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, "KICK " + ChannelName + " " + userName); } /// /// Sends a "ban host" message to the channel. /// /// The host that should be banned. /// The priority of the message in the send queue. public void SendBanMessage(string host, int priority) { connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, string.Format("MODE {0} +b *!*@{1}", ChannelName, host)); } /// /// Changes the channel password. /// /// The new password. If empty, removes the password. /// The priority of the message in the send queue. public void ChangePassword(string newPassword, int priority) { string oldPassword = Password; Password = newPassword; if (string.IsNullOrEmpty(newPassword)) { // remove connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, string.Format("MODE {0} -k {1}", ChannelName, oldPassword)); } else if (string.IsNullOrEmpty(oldPassword)) { // add connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, string.Format("MODE {0} +k {1}", ChannelName, newPassword)); } else { // update (remove + add - both passwords need to be known) connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, string.Format("MODE {0} -k+k {1} {2}", ChannelName, oldPassword, newPassword)); } } public void Join() { // Wait a random amount of time before joining to prevent join/part floods if (Persistent) { int rn = connection.Rng.Next(1, 10000); if (string.IsNullOrEmpty(Password)) connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName); else connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName + " " + Password); } else { if (string.IsNullOrEmpty(Password)) connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName); else connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName + " " + Password); } } public void RequestUserInfo() { connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "WHO " + ChannelName); } public void Leave() { // Wait a random amount of time before joining to prevent join/part floods if (Persistent) { int rn = connection.Rng.Next(1, 10000); connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "PART " + ChannelName); } else { connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "PART " + ChannelName); } ClearUsers(); } public void ClearUsers() { users.Clear(); UserListCleared?.Invoke(this, EventArgs.Empty); } } public class ChannelUserEventArgs : EventArgs { public ChannelUserEventArgs(ChannelUser user) { User = user; } public ChannelUser User { get; private set; } } public class UserNameIndexEventArgs : EventArgs { public UserNameIndexEventArgs(int index, string userName) { UserIndex = index; UserName = userName; } public int UserIndex { get; private set; } public string UserName { get; private set; } } public class UserNameEventArgs : EventArgs { public UserNameEventArgs(string userName) { UserName = userName; } public string UserName { get; private set; } } public class IRCMessageEventArgs : EventArgs { public IRCMessageEventArgs(ChatMessage ircMessage) { Message = ircMessage; } public ChatMessage Message { get; private set; } } public class MessageEventArgs : EventArgs { public MessageEventArgs(string message) { Message = message; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/Online/ChannelUser.cs ================================================ using System; namespace DTAClient.Online { /// /// An user on an IRC channel. /// public class ChannelUser { public ChannelUser(IRCUser ircUser) { IRCUser = ircUser; } public IRCUser IRCUser { get; private set; } public bool IsAdmin { get; set; } public bool IsFriend { get; set; } public bool HasVoice => IRCUser.HasVoice; public static int ChannelUserComparison(ChannelUser u1, ChannelUser u2) { if (u1.IsAdmin != u2.IsAdmin) return u1.IsAdmin ? -1 : 1; if (u1.HasVoice != u2.HasVoice) return u1.HasVoice ? -1 : 1; if (u1.IsFriend != u2.IsFriend) return u1.IsFriend ? -1 : 1; return string.Compare(u1.IRCUser.Name, u2.IRCUser.Name, StringComparison.InvariantCultureIgnoreCase); } } } ================================================ FILE: DXMainClient/Online/ChatMessage.cs ================================================ using Microsoft.Xna.Framework; using System; namespace DTAClient.Online { public class ChatMessage { /// /// Creates a new ChatMessage instance. /// /// The sender of the message. Use null for none (system messages). /// The color of the message. /// The date and time of the message. /// The message. public ChatMessage(string senderName, Color color, DateTime dateTime, string message) { SenderName = senderName; Color = color; DateTime = dateTime; Message = message; } /// /// Creates a chat message with the date and time set to the current system date and time. /// /// The sender of the message. Use null for none (system messages). /// The color of the message. /// The message. public ChatMessage(string senderName, Color color, string message) : this(senderName, color, DateTime.Now, message) { } /// /// Creates a new ChatMessage instance. /// /// The sender of the message. Use null for none (system messages). /// The IRC identifier of the sender. /// The sender of the message is a channel admin. /// The color of the message. /// The date and time of the message. /// The message. public ChatMessage(string senderName, string ident, bool senderIsAdmin, Color color, DateTime dateTime, string message) : this(senderName, color, dateTime, message) { SenderIdent = ident; SenderIsAdmin = senderIsAdmin; } /// /// Creates a chat message that has no sender and has the date and time set to the /// current system date and time. /// /// The color of the message. /// The message. public ChatMessage(Color color, string message) : this(null, color, DateTime.Now, message) { } /// /// Creates a chat message that has no sender and has the date and time set to the /// current system date and time. /// /// The message. public ChatMessage(string message) : this(Color.White, message) { } public string SenderName { get; private set; } public string SenderIdent { get; private set; } public Color Color { get; private set; } public DateTime DateTime { get; private set; } public string Message { get; private set; } public bool SenderIsAdmin { get; private set; } public bool IsUser => SenderIdent != null; } } ================================================ FILE: DXMainClient/Online/CnCNetGameCheck.cs ================================================ using ClientCore; using System.Diagnostics; using System.Threading; namespace DTAClient.Online { public sealed class CnCNetGameCheck { private static readonly CnCNetGameCheck _instance = new CnCNetGameCheck(); private static readonly int REFRESH_INTERVAL = 15000; // 15 seconds private CnCNetGameCheck() { } public static CnCNetGameCheck Instance => _instance; public void InitializeService(CancellationTokenSource cts) { ThreadPool.QueueUserWorkItem(new WaitCallback(RunService), cts); } private void RunService(object tokenObj) { var waitHandle = ((CancellationTokenSource)tokenObj).Token.WaitHandle; while (true) { if (waitHandle.WaitOne(REFRESH_INTERVAL)) { // Cancellation signaled return; } else { CheatEngineWatchEvent(); } } } private void CheatEngineWatchEvent() { if (!ProgramConstants.IsInGame) return; Process[] processlist = Process.GetProcesses(); foreach (Process process in processlist) { try { if (process.ProcessName.Contains("cheatengine") || process.MainWindowTitle.ToLower().Contains("cheat engine") ) { KillGameInstance(); } } catch { } process.Dispose(); } } private void KillGameInstance() { try { string gameExecutableName = ClientConfiguration.Instance.GetOperatingSystemVersion() == OSVersion.UNIX ? ClientConfiguration.Instance.UnixGameExecutableName : ClientConfiguration.Instance.GetGameExecutableName(); gameExecutableName = gameExecutableName.Replace(".exe", ""); Process[] processlist = Process.GetProcesses(); foreach (Process process in processlist) { try { if (process.ProcessName.Contains(gameExecutableName)) { process.Kill(); } } catch { } process.Dispose(); } } catch { } } } } ================================================ FILE: DXMainClient/Online/CnCNetManager.cs ================================================ using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; using ClientCore.Extensions; using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DTAClient.Online { /// /// Acts as an interface between the CnCNet connection class /// and the user-interface's classes. /// public class CnCNetManager : IConnectionManager { // When implementing IConnectionManager functions, pay special attention // to thread-safety. // The functions in IConnectionManager are usually called from the networking // thread, so if they affect anything in the UI or affect data that the // UI thread might be reading, use WindowManager.AddCallback to execute a function // on the UI thread instead of modifying the data or raising events directly. public delegate void UserListDelegate(string channelName, string[] userNames); public event EventHandler WelcomeMessageReceived; public event EventHandler AwayMessageReceived; public event EventHandler WhoReplyReceived; public event EventHandler PrivateMessageReceived; public event EventHandler PrivateCTCPReceived; public event EventHandler BannedFromChannel; public event EventHandler AttemptedServerChanged; public event EventHandler ConnectAttemptFailed; public event EventHandler ConnectionLost; public event EventHandler ReconnectAttempt; public event EventHandler Disconnected; public event EventHandler Connected; public event EventHandler UserAdded; public event EventHandler UserGameIndexUpdated; public event EventHandler UserRemoved; public event EventHandler MultipleUsersAdded; public CnCNetManager(WindowManager wm, GameCollection gc, CnCNetUserData cncNetUserData, Random random) { gameCollection = gc; this.cncNetUserData = cncNetUserData; connection = new Connection(this, random); this.wm = wm; cDefaultChatColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.DefaultChatColor); ircChatColors = new IRCColor[] { new IRCColor("Default color".L10N("Client:Main:ColorDefault"), false, cDefaultChatColor, 0), new IRCColor("Default color #2".L10N("Client:Main:ColorDefault2"), false, cDefaultChatColor, 1), new IRCColor("Light Blue".L10N("Client:Main:ColorLightBlue"), true, Color.LightBlue, 2), new IRCColor("Green".L10N("Client:Main:ColorGreen"), true, Color.ForestGreen, 3), new IRCColor("Dark Red".L10N("Client:Main:ColorDarkRed"), true, new Color(180, 0, 0, 255), 4), new IRCColor("Red".L10N("Client:Main:ColorRed"), true, Color.Red, 5), new IRCColor("Purple".L10N("Client:Main:ColorPurple"), true, Color.MediumPurple, 6), new IRCColor("Orange".L10N("Client:Main:ColorOrange"), true, Color.Orange, 7), new IRCColor("Yellow".L10N("Client:Main:ColorYellow"), true, Color.Yellow, 8), new IRCColor("Lime Green".L10N("Client:Main:ColorLimeGreen"), true, Color.LimeGreen, 9), new IRCColor("Turquoise".L10N("Client:Main:ColorTurquoise"), true, Color.Turquoise, 10), new IRCColor("Sky Blue".L10N("Client:Main:ColorSkyBlue"), true, Color.LightSkyBlue, 11), new IRCColor("Blue".L10N("Client:Main:ColorBlue"), true, Color.RoyalBlue, 12), new IRCColor("Pink".L10N("Client:Main:ColorPink"), true, Color.DeepPink, 13), new IRCColor("Metallic".L10N("Client:Main:ColorLightGrayMetallic"), true, Color.LightGray, 14), new IRCColor("Gray".L10N("Client:Main:ColorGray"), false, Color.Gray, 15) }; } public Channel MainChannel { get; private set; } private bool connected = false; /// /// Gets a value that determines whether the client is /// currently connected to CnCNet. /// public bool IsConnected { get { return connected; } } public bool IsAttemptingConnection { get { return connection.AttemptingConnection; } } /// /// The list of all users that we can see on the IRC network. /// public List UserList = new List(); private Connection connection; private List channels = new List(); private GameCollection gameCollection; private readonly CnCNetUserData cncNetUserData; private Color cDefaultChatColor; private IRCColor[] ircChatColors; private WindowManager wm; private bool disconnect = false; public bool IsCnCNetInitialized() { return Connection.IsIdSet(); } /// /// Factory method for creating a new channel. /// /// The user-interface name of the channel. /// The name of the channel. /// Determines whether the channel's information /// should remain in memory even after a disconnect. /// The password for the channel. Use null for none. /// A channel. public Channel CreateChannel(string uiName, string channelName, bool persistent, bool isChatChannel, string password) { return new Channel(uiName, channelName, persistent, isChatChannel, password, connection); } public void AddChannel(Channel channel) { if (FindChannel(channel.ChannelName) != null) throw new ArgumentException("The channel already exists!".L10N("Client:Main:ChannelExist"), "channel"); channels.Add(channel); } public void RemoveChannel(Channel channel) { if (channel.Persistent) throw new ArgumentException("Persistent channels cannot be removed.".L10N("Client:Main:PersistentChannelRemove"), "channel"); channels.Remove(channel); } public IRCColor[] GetIRCColors() { return ircChatColors; } public void LeaveFromChannel(Channel channel) { connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 10, "PART " + channel.ChannelName); if (!channel.Persistent) channels.Remove(channel); } public void SetMainChannel(Channel channel) { MainChannel = channel; } public void SendCustomMessage(QueuedMessage qm) { connection.QueueMessage(qm); } public void SendWhoIsMessage(string nick) { SendCustomMessage(new QueuedMessage($"WHOIS {nick}", QueuedMessageType.WHOIS_MESSAGE, 0)); } public void OnAttemptedServerChanged(string serverName) { // AddCallback is necessary for thread-safety; OnAttemptedServerChanged // is called by the networking thread, and AddCallback schedules DoAttemptedServerChanged // to be executed on the main (UI) thread. wm.AddCallback(new Action(DoAttemptedServerChanged), serverName); } private void DoAttemptedServerChanged(string serverName) { MainChannel.AddMessage(new ChatMessage( string.Format("Attempting connection to {0}".L10N("Client:Main:AttemptConnectToServer"), serverName))); AttemptedServerChanged?.Invoke(this, new AttemptedServerEventArgs(serverName)); } public void OnAwayMessageReceived(string userName, string reason) { wm.AddCallback(new Action(DoAwayMessageReceived), userName, reason); } private void DoAwayMessageReceived(string userName, string reason) { AwayMessageReceived?.Invoke(this, new UserAwayEventArgs(userName, reason)); } public void OnChannelFull(string channelName) { wm.AddCallback(new Action(DoChannelFull), channelName); } private void DoChannelFull(string channelName) { var channel = FindChannel(channelName); if (channel != null) channel.OnChannelFull(); } public void OnTargetChangeTooFast(string channelName, string message) { wm.AddCallback(new Action(DoTargetChangeTooFast), channelName, message); } private void DoTargetChangeTooFast(string channelName, string message) { var channel = FindChannel(channelName); if (channel != null) channel.OnTargetChangeTooFast(message); } public void OnChannelInviteOnly(string channelName) { wm.AddCallback(new Action(DoChannelInviteOnly), channelName); } private void DoChannelInviteOnly(string channelName) { var channel = FindChannel(channelName); if (channel != null) channel.OnInviteOnlyOnJoin(); } public void OnChannelModesChanged(string userName, string channelName, string modeString, List modeParameters) { wm.AddCallback(new Action>(DoChannelModesChanged), userName, channelName, modeString, modeParameters); } private void DoChannelModesChanged(string userName, string channelName, string modeString, List modeParameters) { Channel channel = FindChannel(channelName); if (channel == null) return; ApplyChannelModes(channel, modeString, modeParameters); channel.OnChannelModesChanged(userName, modeString); } private void ApplyChannelModes(Channel channel, string modeString, List modeParameters) { bool addMode = true; int parameterCount = 0; foreach (char modeChar in modeString) { if (modeChar == '+') addMode = true; else if (modeChar == '-') addMode = false; else { switch (modeChar) { // Add/remove channel operator status on user. case 'o': if (parameterCount >= modeParameters.Count) break; string parameter = modeParameters[parameterCount++]; ChannelUser user = channel.Users.Find(parameter); if (user == null) break; user.IsAdmin = addMode; break; // Add/remove voice status on user. case 'v': if (parameterCount >= modeParameters.Count) break; string vParam = modeParameters[parameterCount++]; ChannelUser vUser = channel.Users.Find(vParam); if (vUser == null) break; vUser.IRCUser.HasVoice = addMode; break; } } } } public void OnChannelTopicReceived(string channelName, string topic) { wm.AddCallback(new Action(DoChannelTopicReceived), channelName, topic); } private void DoChannelTopicReceived(string channelName, string topic) { Channel channel = FindChannel(channelName); if (channel == null) return; channel.Topic = topic; } public void OnChannelTopicChanged(string userName, string channelName, string topic) { wm.AddCallback(new Action(DoChannelTopicReceived), channelName, topic); } public void OnChatMessageReceived(string receiver, string senderName, string ident, string message) { wm.AddCallback(new Action(DoChatMessageReceived), receiver, senderName, ident, message); } private void DoChatMessageReceived(string receiver, string senderName, string ident, string message) { Channel channel = FindChannel(receiver); if (channel == null) return; Color foreColor; // Handle ACTION if (message.Contains("ACTION")) { message = message.Remove(0, 7); message = "====> " + senderName + " " + message; senderName = String.Empty; // Replace Funky's game identifiers with real game names for (int i = 0; i < gameCollection.GameList.Count; i++) { // No localization needed. This message is always in English. // Only the short game identifier is replaced with the full game name; // the surrounding "new ... game" text is left unmodified. message = message.Replace("new " + gameCollection.GetGameIdentifierFromIndex(i) + " game", "new " + gameCollection.GetFullGameNameFromIndex(i) + " game"); } foreColor = Color.White; } else { // Color parsing if (message.Contains(Convert.ToString((char)03))) { if (message.Length < 3) { foreColor = cDefaultChatColor; } else { string colorString = message.Substring(1, 2); message = message.Remove(0, 3); int colorIndex = Conversions.IntFromString(colorString, -1); // Try to parse message color info; if fails, use default color if (colorIndex < ircChatColors.Length && colorIndex > -1) foreColor = ircChatColors[colorIndex].XnaColor; else foreColor = cDefaultChatColor; } } else foreColor = cDefaultChatColor; } if (message.Length > 1 && message[message.Length - 1] == '\u001f') message = message.Remove(message.Length - 1); ChannelUser user = channel.Users.Find(senderName); bool senderIsAdmin = user != null && user.IsAdmin; channel.AddMessage(new ChatMessage(senderName, ident, senderIsAdmin, foreColor, DateTime.Now, message.Replace('\r', ' '))); } public void OnCTCPParsed(string channelName, string userName, string message) { wm.AddCallback(new Action(DoCTCPParsed), channelName, userName, message); } private void DoCTCPParsed(string channelName, string userName, string message) { Channel channel = FindChannel(channelName); // it's possible that we received this CTCP via PRIVMSG, in which case we // expect our username instead of a channel as the first parameter if (channel == null) { if (channelName == ProgramConstants.PLAYERNAME) { PrivateCTCPEventArgs e = new PrivateCTCPEventArgs(userName, message); PrivateCTCPReceived?.Invoke(this, e); } return; } channel.OnCTCPReceived(userName, message); } public void OnConnectAttemptFailed() { wm.AddCallback(new Action(DoConnectAttemptFailed), null); } private void DoConnectAttemptFailed() { ConnectAttemptFailed?.Invoke(this, EventArgs.Empty); MainChannel.AddMessage(new ChatMessage(Color.Red, "Connecting to CnCNet failed!".L10N("Client:Main:ConnectToCncNetFailed"))); } public void OnConnected() { wm.AddCallback(new Action(DoConnected), null); } private void DoConnected() { connected = true; Connected?.Invoke(this, EventArgs.Empty); MainChannel.AddMessage(new ChatMessage("Connection to CnCNet established.".L10N("Client:Main:ConnectToCncNetSuccess"))); } /// /// Called when the connection has got cut un-intentionally. /// /// public void OnConnectionLost(string reason) { wm.AddCallback(new Action(DoConnectionLost), reason); } private void DoConnectionLost(string reason) { ConnectionLost?.Invoke(this, new ConnectionLostEventArgs(reason)); for (int i = 0; i < channels.Count; i++) { if (!channels[i].Persistent) { channels.RemoveAt(i); i--; } else { channels[i].ClearUsers(); } } UserList.Clear(); MainChannel.AddMessage(new ChatMessage(Color.Red, "Connection to CnCNet has been lost.".L10N("Client:Main:ConnectToCncNetHasLost"))); connected = false; } /// /// Disconnects from CnCNet. /// public void Disconnect() { connection.Disconnect(); disconnect = true; } /// /// Connects to CnCNet. /// public void Connect() { disconnect = false; MainChannel.AddMessage(new ChatMessage("Connecting to CnCNet...".L10N("Client:Main:ConnectingToCncNet"))); connection.ConnectAsync(); } /// /// Called when the connection has been aborted intentionally. /// public void OnDisconnected() { wm.AddCallback(new Action(DoDisconnected), null); } private void DoDisconnected() { for (int i = 0; i < channels.Count; i++) { if (!channels[i].Persistent) { channels.RemoveAt(i); i--; } else { channels[i].ClearUsers(); } } MainChannel.AddMessage(new ChatMessage("You have disconnected from CnCNet.".L10N("Client:Main:CncNetDisconnected"))); connected = false; UserList.Clear(); Disconnected?.Invoke(this, EventArgs.Empty); } public void OnErrorReceived(string errorMessage) { MainChannel.AddMessage(new ChatMessage(Color.Red, errorMessage)); } public void OnGenericServerMessageReceived(string message) { wm.AddCallback(new Action(DoGenericServerMessageReceived), message); } private void DoGenericServerMessageReceived(string message) { MainChannel.AddMessage(new ChatMessage(message)); } public void OnIncorrectChannelPassword(string channelName) { wm.AddCallback(new Action(DoIncorrectChannelPassword), channelName); } private void DoIncorrectChannelPassword(string channelName) { var channel = FindChannel(channelName); if (channel != null) channel.OnInvalidJoinPassword(); } public void OnNoticeMessageParsed(string notice, string userName) { // TODO Parse as private message } public void OnPrivateMessageReceived(string sender, string message) { wm.AddCallback(new Action(DoPrivateMessageReceived), sender, message); } private void DoPrivateMessageReceived(string sender, string message) { CnCNetPrivateMessageEventArgs e = new CnCNetPrivateMessageEventArgs(sender, message); PrivateMessageReceived?.Invoke(this, e); } public void OnReconnectAttempt() { wm.AddCallback(new Action(DoReconnectAttempt), null); } private void DoReconnectAttempt() { ReconnectAttempt?.Invoke(this, EventArgs.Empty); MainChannel.AddMessage(new ChatMessage("Attempting to reconnect to CnCNet...".L10N("Client:Main:ReconnectingCncNet"))); connection.ConnectAsync(); } public void OnUserJoinedChannel(string channelName, string host, string userName, string ident) { wm.AddCallback(new Action(DoUserJoinedChannel), channelName, host, userName, ident); } private void DoUserJoinedChannel(string channelName, string host, string userName, string userAddress) { Channel channel = FindChannel(channelName); if (channel == null) return; bool isAdmin = false; string name = userName; if (userName.StartsWith("@")) { isAdmin = true; name = userName.Remove(0, 1); } IRCUser ircUser = null; // Check if we already know this user from another channel // Avoid LINQ here for performance reasons foreach (var user in UserList) { if (user.Name == name) { ircUser = (IRCUser)user.Clone(); break; } } // If we don't know the user, create a new one if (ircUser == null) { string identifier = userAddress.Split('@')[0]; string[] parts = identifier.Split('.'); ircUser = new IRCUser(name, identifier, host); if (parts.Length > 1) { ircUser.GameID = gameCollection.GameList.FindIndex(g => g.InternalName.ToUpper() == parts[0].Replace("~", string.Empty)); } AddUserToGlobalUserList(ircUser); } var channelUser = new ChannelUser(ircUser); channelUser.IsAdmin = isAdmin; channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); ircUser.Channels.Add(channelName); channel.OnUserJoined(channelUser); //UserJoinedChannel?.Invoke(this, new ChannelUserEventArgs(channelName, userName)); } private void AddUserToGlobalUserList(IRCUser user) { UserList.Add(user); UserList = UserList.OrderBy(u => u.Name).ToList(); UserAdded?.Invoke(this, new UserEventArgs(user)); } public void OnUserKicked(string channelName, string userName) { wm.AddCallback(new Action(DoUserKicked), channelName, userName); } private void DoUserKicked(string channelName, string userName) { Channel channel = FindChannel(channelName); if (channel == null) return; channel.OnUserKicked(userName); if (userName == ProgramConstants.PLAYERNAME) { channel.Users.DoForAllUsers(user => { RemoveChannelFromUser(user.IRCUser.Name, channelName); }); if (!channel.Persistent) channels.Remove(channel); channel.ClearUsers(); return; } RemoveChannelFromUser(userName, channelName); } public void OnUserLeftChannel(string channelName, string userName) { wm.AddCallback(new Action(DoUserLeftChannel), channelName, userName); } private void DoUserLeftChannel(string channelName, string userName) { Channel channel = FindChannel(channelName); if (channel == null) return; channel.OnUserLeft(userName); if (userName == ProgramConstants.PLAYERNAME) { channel.Users.DoForAllUsers(user => { RemoveChannelFromUser(user.IRCUser.Name, channelName); }); if (!channel.Persistent) channels.Remove(channel); channel.ClearUsers(); return; } RemoveChannelFromUser(userName, channelName); } /// /// Looks up an user in the global user list and removes a channel from the user. /// If the user is left with 0 channels (meaning we have no common channel with the user), /// the user is removed from the global user list. /// /// The name of the user. /// The name of the channel. public void RemoveChannelFromUser(string userName, string channelName) { var userIndex = UserList.FindIndex(user => user.Name.ToLower() == userName.ToLower()); if (userIndex > -1) { var ircUser = UserList[userIndex]; ircUser.Channels.Remove(channelName); if (ircUser.Channels.Count == 0) { UserList.RemoveAt(userIndex); UserRemoved?.Invoke(this, new UserNameIndexEventArgs(userIndex, userName)); } } } public void OnUserListReceived(string channelName, string[] userList) { wm.AddCallback(new UserListDelegate(DoUserListReceived), channelName, userList); } private void DoUserListReceived(string channelName, string[] userList) { Channel channel = FindChannel(channelName); if (channel == null) return; var channelUserList = new List(); foreach (string userName in userList) { string name = userName; bool isAdmin = false; bool hasVoice = false; if (userName.StartsWith("@")) { isAdmin = true; name = userName.Substring(1); } else if (userName.StartsWith("+")) { hasVoice = true; name = userName.Substring(1); } // Check if we already know the IRC user from another channel IRCUser ircUser = UserList.Find(u => u.Name == name); // If the user isn't familiar to us already, // create a new user instance and add it to the global user list if (ircUser == null) { ircUser = new IRCUser(name); UserList.Add(ircUser); } var channelUser = new ChannelUser(ircUser); channelUser.IsAdmin = isAdmin; channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); channelUser.IRCUser.HasVoice = hasVoice; channelUserList.Add(channelUser); } UserList = UserList.OrderBy(u => u.Name).ToList(); MultipleUsersAdded?.Invoke(this, EventArgs.Empty); channel.OnUserListReceived(channelUserList); } public void OnUserQuitIRC(string userName) { wm.AddCallback(new Action(DoUserQuitIRC), userName); } private void DoUserQuitIRC(string userName) { new List(channels).ForEach(ch => ch.OnUserQuitIRC(userName)); int userIndex = UserList.FindIndex(user => user.Name == userName); if (userIndex > -1) { UserList.RemoveAt(userIndex); UserRemoved?.Invoke(this, new UserNameIndexEventArgs(userIndex, userName)); } } public void OnWelcomeMessageReceived(string message) { wm.AddCallback(new Action(DoWelcomeMessageReceived), message); } /// /// Finds a channel with the specified internal name, case-insensitively. /// /// The internal name of the channel. /// A channel if one matching the name is found, otherwise null. public Channel FindChannel(string channelName) { channelName = channelName.ToLower(); foreach (var channel in channels) { if (channel.ChannelName.ToLower() == channelName) return channel; } return null; } private void DoWelcomeMessageReceived(string message) { channels.ForEach(ch => ch.AddMessage(new ChatMessage(message))); WelcomeMessageReceived?.Invoke(this, new ServerMessageEventArgs(message)); } public void OnWhoReplyReceived(string ident, string hostName, string userName, string extraInfo) { wm.AddCallback(new Action(DoWhoReplyReceived), ident, hostName, userName, extraInfo); } private void DoWhoReplyReceived(string ident, string hostName, string userName, string extraInfo) { WhoReplyReceived?.Invoke(this, new WhoEventArgs(ident, userName, extraInfo)); string[] eInfoParts = extraInfo.Split(' '); int gameIndex = -1; if (eInfoParts.Length > 2) { string gameName = eInfoParts[2]; gameIndex = gameCollection.GetGameIndexFromInternalName(gameName); if (gameIndex == -1) return; } var user = UserList.Find(u => u.Name == userName); if (user != null) { user.GameID = gameIndex; user.Ident = ident; user.Hostname = hostName; if (gameIndex != -1) { channels.ForEach(ch => ch.UpdateGameIndexForUser(userName)); UserGameIndexUpdated?.Invoke(this, new UserEventArgs(user)); } } } public bool GetDisconnectStatus() { return disconnect; } public void OnNameAlreadyInUse() { wm.AddCallback(new Action(DoNameAlreadyInUse), null); } /// /// Handles situations when the requested name is already in use by another /// IRC user. Adds additional underscores to the name or replaces existing /// characters with underscores. /// private void DoNameAlreadyInUse() { var charList = ProgramConstants.PLAYERNAME.ToList(); int maxNameLength = ClientConfiguration.Instance.MaxNameLength; if (charList.Count < maxNameLength) charList.Add('_'); else { int lastNonUnderscoreIndex = charList.FindLastIndex(c => c != '_'); if (lastNonUnderscoreIndex == -1) { MainChannel.AddMessage(new ChatMessage(Color.White, "Your nickname is invalid or already in use. Please change your nickname in the login screen.".L10N("Client:Main:PickAnotherNickName"))); UserINISettings.Instance.SkipConnectDialog.Value = false; Disconnect(); return; } charList[lastNonUnderscoreIndex] = '_'; } var sb = new StringBuilder(); foreach (char c in charList) sb.Append(c); MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Your name is already in use. Retrying with {0}...".L10N("Client:Main:NameInUseRetry"), sb.ToString()))); ProgramConstants.PLAYERNAME = sb.ToString(); connection.ChangeNickname(); } public void OnBannedFromChannel(string channelName) { wm.AddCallback(new Action(DoBannedFromChannel), channelName); } private void DoBannedFromChannel(string channelName) { BannedFromChannel?.Invoke(this, new ChannelEventArgs(channelName)); } public void OnUserNicknameChange(string oldNickname, string newNickname) => wm.AddCallback(new Action(DoUserNicknameChange), oldNickname, newNickname); private void DoUserNicknameChange(string oldNickname, string newNickname) { IRCUser user = UserList.Find(u => u.Name.ToUpper() == oldNickname.ToUpper()); if (user == null) { Logger.Log("DoUserNicknameChange: Failed to find user with nickname " + oldNickname); return; } string realOldNickname = user.Name; // To make sure that case matches user.Name = newNickname; channels.ForEach(ch => ch.OnUserNameChanged(realOldNickname, newNickname)); } public void OnServerLatencyTested(int candidateCount, int closerCount) { wm.AddCallback(new Action(DoServerLatencyTested), candidateCount, closerCount); } private void DoServerLatencyTested(int candidateCount, int closerCount) { MainChannel.AddMessage(new ChatMessage( string.Format( "Lobby servers: {0} available, {1} fast.".L10N("Client:Main:LobbyServerLatencyTestResult"), candidateCount, closerCount))); } } public class UserEventArgs : EventArgs { public UserEventArgs(IRCUser ircUser) { User = ircUser; } public IRCUser User { get; private set; } } public class IndexEventArgs : EventArgs { public IndexEventArgs(int index) { Index = index; } public int Index { get; private set; } } public class UserNameChangedEventArgs : EventArgs { public UserNameChangedEventArgs(string oldUserName, IRCUser user) { OldUserName = oldUserName; User = user; } public string OldUserName { get; } public IRCUser User { get; } } } ================================================ FILE: DXMainClient/Online/CnCNetUserData.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; namespace DTAClient.Online { public sealed class CnCNetUserData { private const string FRIEND_LIST_PATH = "Client/friend_list"; private const string IGNORE_LIST_PATH = "Client/ignore_list"; private const string RECENT_LIST_PATH = "Client/recent_list"; private const int RECENT_LIMIT = 50; private readonly Lazy> lazyFriendList; /// /// A list which contains names of friended users. If you manipulate this list /// directly you have to also invoke UserFriendToggled event handler for every /// user name added or removed. /// public List FriendList => lazyFriendList.Value; private readonly Lazy> lazyIgnoreList; /// /// A list which contains idents of ignored users. If you manipulate this list /// directly you have to also invoke UserIgnoreToggled event handler for every /// user ident added or removed. /// public List IgnoreList => lazyIgnoreList.Value; private readonly Lazy> lazyRecentList; /// /// A list which contains names of players from recent games. /// public List RecentList => lazyRecentList.Value; public event EventHandler UserFriendToggled; public event EventHandler UserIgnoreToggled; public CnCNetUserData(WindowManager windowManager) { lazyFriendList = new Lazy>(LoadFriendList, LazyThreadSafetyMode.ExecutionAndPublication); lazyIgnoreList = new Lazy>(LoadIgnoreList, LazyThreadSafetyMode.ExecutionAndPublication); lazyRecentList = new Lazy>(LoadRecentPlayerList, LazyThreadSafetyMode.ExecutionAndPublication); // Load lists in background. Fire-and-forget. Task.Run(() => _ = FriendList); Task.Run(() => _ = IgnoreList); Task.Run(() => _ = RecentList); windowManager.GameClosing += WindowManager_GameClosing; } private static List LoadTextList(string path) { try { FileInfo listFile = SafePath.GetFile(ProgramConstants.GamePath, path); if (listFile.Exists) return File.ReadAllLines(listFile.FullName).ToList(); Logger.Log($"Loading {path} failed! File does not exist."); return new(); } catch { Logger.Log($"Loading {path} list failed!"); return new(); } } private static List LoadJsonList(string path) { try { FileInfo listFile = SafePath.GetFile(ProgramConstants.GamePath, path); if (listFile.Exists) return JsonSerializer.Deserialize>(File.ReadAllText(listFile.FullName)) ?? new List(); Logger.Log($"Loading {path} failed! File does not exist."); return new(); } catch { Logger.Log($"Loading {path} list failed!"); return new(); } } private static void SaveTextList(string path, List textList) { Logger.Log($"Saving {path}."); try { FileInfo listFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path); listFileInfo.Delete(); File.WriteAllLines(listFileInfo.FullName, textList.ToArray()); } catch (Exception ex) { Logger.Log($"Saving {path} failed! Error message: " + ex.ToString()); } } private static void SaveJsonList(string path, IReadOnlyCollection jsonList) { Logger.Log($"Saving {path}."); try { FileInfo listFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path); listFileInfo.Delete(); File.WriteAllText(listFileInfo.FullName, JsonSerializer.Serialize(jsonList)); } catch (Exception ex) { Logger.Log($"Saving {path} failed! Error message: " + ex.ToString()); } } private static void Toggle(string value, ICollection list) { if (string.IsNullOrEmpty(value)) return; if (list.Contains(value)) list.Remove(value); else list.Add(value); } private List LoadFriendList() => LoadTextList(FRIEND_LIST_PATH); private List LoadIgnoreList() => LoadTextList(IGNORE_LIST_PATH); private List LoadRecentPlayerList() => LoadJsonList(RECENT_LIST_PATH); private void WindowManager_GameClosing(object sender, EventArgs e) => Save(); private void SaveFriends() => SaveTextList(FRIEND_LIST_PATH, FriendList); private void SaveIgnoreList() => SaveTextList(IGNORE_LIST_PATH, IgnoreList); private void SaveRecentList() => SaveJsonList(RECENT_LIST_PATH, RecentList); private void Save() { SaveFriends(); SaveIgnoreList(); SaveRecentList(); } /// /// Adds or removes a specified user to or from the friend list /// depending on whether they already are on the friend list. /// /// The name of the user. public void ToggleFriend(string name) { Toggle(name, FriendList); UserFriendToggled?.Invoke(this, new(name)); } /// /// Adds or removes a specified user to or from the chat ignore list /// depending on whether they already are on the ignore list. /// /// The ident of the IRCUser. public void ToggleIgnoreUser(string ident) { Toggle(ident, IgnoreList); UserIgnoreToggled?.Invoke(this, new(ident)); } public void AddRecentPlayers(IEnumerable recentPlayerNames, string gameName) { recentPlayerNames = recentPlayerNames.Where(name => name != ProgramConstants.PLAYERNAME); var now = DateTime.UtcNow; RecentList.AddRange(recentPlayerNames.Select(rp => new RecentPlayer() { PlayerName = rp, GameName = gameName, GameTime = now })); int skipCount = Math.Max(0, RecentList.Count - RECENT_LIMIT); RecentList.RemoveRange(0, skipCount); } /// /// Checks to see if a user is in the ignore list. /// /// The IRC identifier of the user. public bool IsIgnored(string ident) => IgnoreList.Contains(ident); /// /// Checks if a specified user belongs to the friend list. /// /// The name of the user. public bool IsFriend(string name) => FriendList.Contains(name); } public sealed class IdentEventArgs : EventArgs { public IdentEventArgs(string ident) { Ident = ident; } public string Ident { get; } } } ================================================ FILE: DXMainClient/Online/Connection.cs ================================================ using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DTAClient.Online { /// /// The CnCNet connection handler. /// public class Connection { private const int MAX_RECONNECT_COUNT = 8; private const int MAX_ERROR_COUNT = 30; private const int RECONNECT_WAIT_DELAY = 4000; private const int ID_LENGTH = 9; private const int MAXIMUM_LATENCY = 400; private const int BYTE_ARRAY_MSG_LEN = 1024; public Connection(IConnectionManager connectionManager, Random random) { this.connectionManager = connectionManager; this.Rng = random; } IConnectionManager connectionManager; public Random Rng; private static IList _servers = null; /// /// The list of CnCNet / GameSurge IRC servers to connect to. /// private static IList Servers { get { if (_servers is not null) return _servers; IEnumerable serversList; if (ClientConfiguration.Instance.IRCServers.Count > 0) serversList = ClientConfiguration.Instance.IRCServers; else { // fallback to the hardcoded servers list serversList = [ "irc.gamesurge.net|GameSurge|6667,6660,6666,6668,6669", ]; } _servers = serversList.Select(Server.Deserialize).ToList(); return _servers; } } bool _isConnected = false; public bool IsConnected { get { return _isConnected; } } bool _attemptingConnection = false; public bool AttemptingConnection { get { return _attemptingConnection; } } private List MessageQueue = new List(); private TimeSpan MessageQueueDelay; private NetworkStream serverStream; private TcpClient tcpClient; volatile int reconnectCount = 0; private volatile bool connectionCut = false; private volatile bool welcomeMessageReceived = false; private volatile bool sendQueueExited = false; bool _disconnect = false; private bool disconnect { get { lock (locker) return _disconnect; } set { lock (locker) _disconnect = value; } } private string overMessage; private readonly Encoding encoding = Encoding.UTF8; /// /// A list of server IPs that have dropped our connection. /// The client skips these servers when attempting to re-connect, to /// prevent a server that first accepts a connection and then drops it /// right afterwards from preventing online play. /// private readonly List failedServerIPs = new List(); private volatile string currentConnectedServerIP; private static readonly object locker = new object(); private static readonly object messageQueueLocker = new object(); private static bool idSet = false; private static string systemId; private static readonly object idLocker = new object(); public static void SetId(string id) { lock (idLocker) { int maxLength = ID_LENGTH - (ClientConfiguration.Instance.LocalGame.Length + 1); systemId = Utilities.CalculateSHA1ForString(id).Substring(0, maxLength); idSet = true; } } public static bool IsIdSet() { lock (idLocker) { return idSet; } } /// /// Attempts to connect to CnCNet without blocking the calling thread. /// public void ConnectAsync() { if (_isConnected) throw new InvalidOperationException("The client is already connected!"); if (_attemptingConnection) return; // Maybe we should throw in this case as well? welcomeMessageReceived = false; connectionCut = false; _attemptingConnection = true; disconnect = false; MessageQueueDelay = TimeSpan.FromMilliseconds(ClientConfiguration.Instance.SendSleep); Thread connection = new Thread(ConnectToServer); connection.Start(); } /// /// Attempts to connect to CnCNet. /// private void ConnectToServer() { IList sortedServerList = GetServerListSortedByLatency(); foreach (Server server in sortedServerList) { try { for (int i = 0; i < server.Ports.Length; i++) { connectionManager.OnAttemptedServerChanged(server.Name); TcpClient client = new TcpClient(AddressFamily.InterNetwork); var result = client.BeginConnect(server.Host, server.Ports[i], null, null); result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3), false); Logger.Log("Attempting connection to " + server.Host + ":" + server.Ports[i]); if (!client.Connected) { Logger.Log("Connecting to " + server.Host + " port " + server.Ports[i] + " timed out!"); continue; // Start all over again, using the next port } Logger.Log("Succesfully connected to " + server.Host + " on port " + server.Ports[i]); client.EndConnect(result); _isConnected = true; _attemptingConnection = false; connectionManager.OnConnected(); Thread sendQueueHandler = new Thread(RunSendQueue); sendQueueHandler.Start(); tcpClient = client; serverStream = tcpClient.GetStream(); serverStream.ReadTimeout = 1000; currentConnectedServerIP = server.Host; HandleComm(); return; } } catch (Exception ex) { Logger.Log("Unable to connect to the server. " + ex.ToString()); } } Logger.Log("Connecting to CnCNet failed!"); // Clear the failed server list in case connecting to all servers has failed failedServerIPs.Clear(); _attemptingConnection = false; connectionManager.OnConnectAttemptFailed(); } private void HandleComm() { int errorTimes = 0; byte[] message = new byte[BYTE_ARRAY_MSG_LEN]; Register(); Timer timer = new Timer(AutoPing, null, 30000, 120000); connectionCut = true; while (true) { if (connectionManager.GetDisconnectStatus()) { connectionManager.OnDisconnected(); connectionCut = false; // This disconnect is intentional break; } int bytesRead = 0; try { bytesRead = serverStream.Read(message, 0, BYTE_ARRAY_MSG_LEN); } catch (IOException ex) { errorTimes++; if (errorTimes > MAX_ERROR_COUNT) { const string errorMessage = "Disconnected from CnCNet after not receiving a packet for too long."; Logger.Log(errorMessage + Environment.NewLine + "Message: " + ex.ToString()); failedServerIPs.Add(currentConnectedServerIP); connectionManager.OnConnectionLost(errorMessage.L10N("Client:Main:ClientDisconnectedAfterRetries")); break; } continue; } catch (Exception ex) { const string errorMessage = "Disconnected from CnCNet due to an internal error."; Logger.Log(errorMessage + Environment.NewLine + "Message: " + ex.ToString()); failedServerIPs.Add(currentConnectedServerIP); connectionManager.OnConnectionLost(errorMessage.L10N("Client:Main:ClientDisconnectedAfterException")); break; } if (bytesRead == 0) { errorTimes++; if (errorTimes > MAX_ERROR_COUNT) { Logger.Log("Disconnected from CnCNet."); failedServerIPs.Add(currentConnectedServerIP); connectionManager.OnConnectionLost("Server disconnected.".L10N("Client:Main:ServerDisconnected")); break; } continue; } errorTimes = 0; // A message has been succesfully received string msg = encoding.GetString(message, 0, bytesRead); Logger.Log("Message received: " + msg); HandleMessage(msg); timer.Change(30000, 30000); } timer.Change(Timeout.Infinite, Timeout.Infinite); timer.Dispose(); _isConnected = false; disconnect = false; if (connectionCut) { while (!sendQueueExited) Thread.Sleep(100); reconnectCount++; if (reconnectCount > MAX_RECONNECT_COUNT) { Logger.Log("Reconnect attempt count exceeded!"); return; } Thread.Sleep(RECONNECT_WAIT_DELAY); if (IsConnected || AttemptingConnection) { Logger.Log("Cancelling reconnection attempt because the user has attempted to reconnect manually."); return; } Logger.Log("Attempting to reconnect to CnCNet."); connectionManager.OnReconnectAttempt(); } } /// /// Get all IP addresses of Lobby servers by resolving the hostname and test the latency to the servers. /// The maximum latency is defined in MAXIMUM_LATENCY, see . /// Servers that did not respond to ICMP messages in time will be placed at the end of the list. /// /// A list of Lobby servers sorted by latency. private IList GetServerListSortedByLatency() { // Resolve the hostnames. ICollection>>> dnsTasks = new List>>>(Servers.Count); foreach (Server server in Servers) { string serverHostnameOrIPAddress = server.Host; string serverName = server.Name; int[] serverPorts = server.Ports; Task>> dnsTask = new Task>>(() => { Logger.Log($"Attempting to DNS resolve {serverName} ({serverHostnameOrIPAddress})."); ICollection> _serverInfos = new List>(); try { // If hostNameOrAddress is an IP address, this address is returned without querying the DNS server. IEnumerable serverIPAddresses = Dns.GetHostAddresses(serverHostnameOrIPAddress) .Where(IPAddress => IPAddress.AddressFamily == AddressFamily.InterNetwork); Logger.Log($"DNS resolved {serverName} ({serverHostnameOrIPAddress}): " + $"{string.Join(", ", serverIPAddresses.Select(item => item.ToString()))}"); // Store each IPAddress in a different tuple. foreach (IPAddress serverIPAddress in serverIPAddresses) { _serverInfos.Add(new Tuple(serverIPAddress, serverName, serverPorts)); } } catch (SocketException ex) { Logger.Log($"Caught an exception when DNS resolving {serverName} ({serverHostnameOrIPAddress}) Lobby server: {ex.ToString()}"); } return _serverInfos; }); dnsTask.Start(); dnsTasks.Add(dnsTask); } Task.WaitAll(dnsTasks.ToArray()); // Group the tuples by IPAddress to merge duplicate servers. IEnumerable>> serverInfosGroupedByIPAddress = dnsTasks.SelectMany(dnsTask => dnsTask.Result) // Tuple .GroupBy( serverInfo => serverInfo.Item1, // IPAddress serverInfo => new Tuple( serverInfo.Item2, // serverName serverInfo.Item3 // serverPorts ) ); // Process each group: // 1. Get IPAddress. // 2. Concatenate serverNames. // 3. Remove duplicate ports. // 4. Construct and return a tuple that contains the IPAddress, concatenated serverNames and unique ports. IEnumerable> serverInfos = serverInfosGroupedByIPAddress.Select(serverInfoGroup => { IPAddress ipAddress = serverInfoGroup.Key; string serverNames = string.Join(", ", serverInfoGroup.Select(serverInfo => serverInfo.Item1)); int[] serverPorts = serverInfoGroup.SelectMany(serverInfo => serverInfo.Item2).Distinct().ToArray(); return new Tuple(ipAddress, serverNames, serverPorts); }); // Do logging. foreach (Tuple serverInfo in serverInfos) { string serverIPAddress = serverInfo.Item1.ToString(); string serverNames = string.Join(", ", serverInfo.Item2.ToString()); string serverPorts = string.Join(", ", serverInfo.Item3.Select(port => port.ToString())); Logger.Log($"Got a Lobby server. IP: {serverIPAddress}; Name: {serverNames}; Ports: {serverPorts}."); } Logger.Log($"The number of Lobby servers is {serverInfos.Count()}."); // Test the latency. ICollection>> pingTasks = new List>>(serverInfos.Count()); foreach (Tuple serverInfo in serverInfos) { IPAddress serverIPAddress = serverInfo.Item1; string serverNames = serverInfo.Item2; int[] serverPorts = serverInfo.Item3; if (failedServerIPs.Contains(serverIPAddress.ToString())) { Logger.Log($"Skipped a failed server {serverNames} ({serverIPAddress})."); continue; } Task> pingTask = new Task>(() => { Logger.Log($"Attempting to ping {serverNames} ({serverIPAddress})."); Server server = new Server(serverIPAddress.ToString(), serverNames, serverPorts); using (Ping ping = new Ping()) { try { PingReply pingReply = ping.Send(serverIPAddress, MAXIMUM_LATENCY); if (pingReply.Status == IPStatus.Success) { long pingInMs = pingReply.RoundtripTime; Logger.Log($"The latency in milliseconds to the server {serverNames} ({serverIPAddress}): {pingInMs}."); return new Tuple(server, pingInMs); } else { Logger.Log($"Failed to ping the server {serverNames} ({serverIPAddress}): " + $"{Enum.GetName(typeof(IPStatus), pingReply.Status)}."); return new Tuple(server, long.MaxValue); } } catch (PingException ex) { Logger.Log($"Caught an exception when pinging {serverNames} ({serverIPAddress}) Lobby server: {ex.ToString()}"); return new Tuple(server, long.MaxValue); } } }); pingTask.Start(); pingTasks.Add(pingTask); } Task.WaitAll(pingTasks.ToArray()); // Sort the servers by latency. IOrderedEnumerable> sortedServerAndLatencyResults = pingTasks.Select(task => task.Result) // Tuple .OrderBy(taskResult => taskResult.Item2); // Latency // Do logging. foreach (Tuple serverAndLatencyResult in sortedServerAndLatencyResults) { string serverIPAddress = serverAndLatencyResult.Item1.Host; long serverLatencyValue = serverAndLatencyResult.Item2; string serverLatencyString = serverLatencyValue <= MAXIMUM_LATENCY ? serverLatencyValue.ToString() : "DNF"; Logger.Log($"Lobby server IP: {serverIPAddress}, latency: {serverLatencyString}."); } { int candidateCount = sortedServerAndLatencyResults.Count(); int closerCount = sortedServerAndLatencyResults.Count( serverAndLatencyResult => serverAndLatencyResult.Item2 <= MAXIMUM_LATENCY); Logger.Log($"Lobby servers: {candidateCount} available, {closerCount} fast."); connectionManager.OnServerLatencyTested(candidateCount, closerCount); } return sortedServerAndLatencyResults.Select(taskResult => taskResult.Item1).ToList(); // Server } public void Disconnect() { disconnect = true; SendMessage("QUIT"); tcpClient.Close(); serverStream.Close(); } #region Handling commands /// /// Checks if a message from the IRC server is a partial or full /// message, and handles it accordingly. /// /// The message. private void HandleMessage(string message) { string msg = overMessage + message; overMessage = ""; while (true) { int commandEndIndex = msg.IndexOf("\n"); if (commandEndIndex == -1) { overMessage = msg; break; } else if (msg.Length != commandEndIndex + 1) { string command = msg.Substring(0, commandEndIndex - 1); PerformCommand(command); msg = msg.Remove(0, commandEndIndex + 1); } else { string command = msg.Substring(0, msg.Length - 1); PerformCommand(command); break; } } } /// /// Handles a specific command received from the IRC server. /// private void PerformCommand(string message) { string prefix = String.Empty; string command = String.Empty; message = message.Replace("\r", String.Empty); List parameters = new List(); ParseIrcMessage(message, out prefix, out command, out parameters); string paramString = String.Empty; foreach (string param in parameters) { paramString = paramString + param + ","; } Logger.Log("RMP: " + prefix + " " + command + " " + paramString); try { bool success = false; int commandNumber = -1; success = Int32.TryParse(command, out commandNumber); if (success) { string serverMessagePart = prefix + ": "; switch (commandNumber) { // Command descriptions from https://www.alien.net.au/irc/irc2numerics.html case 001: // Welcome message message = serverMessagePart + parameters[1]; welcomeMessageReceived = true; connectionManager.OnWelcomeMessageReceived(message); reconnectCount = 0; break; case 002: // "Your host is x, running version y" case 003: // "This server was created..." case 251: // There are users and invisible on servers case 255: // I have clients and servers case 265: // Local user count case 266: // Global user count case 401: // Used to indicate the nickname parameter supplied to a command is currently unused case 403: // Used to indicate the given channel name is invalid, or does not exist case 404: // Used to indicate that the user does not have the rights to send a message to a channel case 432: // Invalid nickname on registration case 461: // Returned by the server to any command which requires more parameters than the number of parameters given case 465: // Returned to a client after an attempt to register on a server configured to ban connections from that client StringBuilder displayedMessage = new StringBuilder(serverMessagePart); for (int i = 1; i < parameters.Count; i++) { displayedMessage.Append(' '); displayedMessage.Append(parameters[i]); } connectionManager.OnGenericServerMessageReceived(displayedMessage.ToString()); break; case 439: // Attempt to send messages too fast connectionManager.OnTargetChangeTooFast(parameters[1], parameters[2]); break; case 252: // Number of operators online case 254: // Number of channels formed message = serverMessagePart + parameters[1] + " " + parameters[2]; connectionManager.OnGenericServerMessageReceived(message); break; case 301: // AWAY message string awayTarget = parameters[0]; if (awayTarget != ProgramConstants.PLAYERNAME) break; string awayPlayer = parameters[1]; string awayReason = parameters[2]; connectionManager.OnAwayMessageReceived(awayPlayer, awayReason); break; case 332: // Channel topic message string _target = parameters[0]; if (_target != ProgramConstants.PLAYERNAME) break; connectionManager.OnChannelTopicReceived(parameters[1], parameters[2]); break; case 353: // User list (reply to NAMES) string target = parameters[0]; if (target != ProgramConstants.PLAYERNAME) break; string channelName = parameters[2]; string[] users = parameters[3].Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); connectionManager.OnUserListReceived(channelName, users); break; case 352: // Reply to WHO query string ident = parameters[2]; string host = parameters[3]; string wUserName = parameters[5]; string extraInfo = parameters[7]; connectionManager.OnWhoReplyReceived(ident, host, wUserName, extraInfo); break; case 311: // Reply to WHOIS NAME query connectionManager.OnWhoReplyReceived(parameters[2], parameters[3], parameters[1], string.Empty); break; case 433: // Name already in use message = serverMessagePart + parameters[1] + ": " + parameters[2]; //connectionManager.OnGenericServerMessageReceived(message); connectionManager.OnNameAlreadyInUse(); break; case 451: // Not registered Register(); connectionManager.OnGenericServerMessageReceived(message); break; case 471: // Returned when attempting to join a channel that is full (basically, player limit met) connectionManager.OnChannelFull(parameters[1]); break; case 473: // Returned when attempting to join an invite-only channel (locked games) connectionManager.OnChannelInviteOnly(parameters[1]); break; case 474: // Returned when attempting to join a channel a user is banned from connectionManager.OnBannedFromChannel(parameters[1]); break; case 475: // Returned when attempting to join a key-locked channel either without a key or with the wrong key connectionManager.OnIncorrectChannelPassword(parameters[1]); break; } return; } switch (command) { case "NOTICE": int noticeExclamIndex = prefix.IndexOf('!'); if (noticeExclamIndex > -1) { if (parameters.Count > 1 && parameters[1][0] == 1)//Conversions.IntFromString(parameters[1].Substring(0, 1), -1) == 1) { // CTCP string channelName = parameters[0]; string ctcpMessage = parameters[1]; ctcpMessage = ctcpMessage.Remove(0, 1).Remove(ctcpMessage.Length - 2); string ctcpSender = prefix.Substring(0, noticeExclamIndex); connectionManager.OnCTCPParsed(channelName, ctcpSender, ctcpMessage); return; } else { string noticeUserName = prefix.Substring(0, noticeExclamIndex); string notice = parameters[parameters.Count - 1]; connectionManager.OnNoticeMessageParsed(notice, noticeUserName); break; } } string noticeParamString = String.Empty; foreach (string param in parameters) noticeParamString = noticeParamString + param + " "; connectionManager.OnGenericServerMessageReceived(prefix + " " + noticeParamString); break; case "JOIN": string channel = parameters[0]; int atIndex = prefix.IndexOf('@'); int exclamIndex = prefix.IndexOf('!'); string userName = prefix.Substring(0, exclamIndex); string ident = prefix.Substring(exclamIndex + 1, atIndex - (exclamIndex + 1)); string host = prefix.Substring(atIndex + 1); connectionManager.OnUserJoinedChannel(channel, host, userName, ident); break; case "PART": string pChannel = parameters[0]; string pUserName = prefix.Substring(0, prefix.IndexOf('!')); connectionManager.OnUserLeftChannel(pChannel, pUserName); break; case "QUIT": string qUserName = prefix.Substring(0, prefix.IndexOf('!')); connectionManager.OnUserQuitIRC(qUserName); break; case "PRIVMSG": if (parameters.Count > 1 && Convert.ToInt32(parameters[1][0]) == 1 && !parameters[1].Contains("ACTION")) { goto case "NOTICE"; } string pmsgUserName = prefix.Substring(0, prefix.IndexOf('!')); string pmsgIdent = GetIdentFromPrefix(prefix); string[] recipients = new string[parameters.Count - 1]; for (int pid = 0; pid < parameters.Count - 1; pid++) recipients[pid] = parameters[pid]; string privmsg = parameters[parameters.Count - 1]; if (parameters[1].StartsWith('\u0001'.ToString() + "ACTION")) privmsg = privmsg.Substring(1).Remove(privmsg.Length - 2); foreach (string recipient in recipients) { if (recipient.StartsWith("#")) connectionManager.OnChatMessageReceived(recipient, pmsgUserName, pmsgIdent, privmsg); else if (recipient == ProgramConstants.PLAYERNAME) connectionManager.OnPrivateMessageReceived(pmsgUserName, privmsg); //else if (pmsgUserName == ProgramConstants.PLAYERNAME) //{ // DoPrivateMessageSent(privmsg, recipient); //} } break; case "MODE": string modeUserName = prefix.Substring(0, prefix.IndexOf('!')); string modeChannelName = parameters[0]; string modeString = parameters[1]; List modeParameters = parameters.Count > 2 ? parameters.GetRange(2, parameters.Count - 2) : new List(); connectionManager.OnChannelModesChanged(modeUserName, modeChannelName, modeString, modeParameters); break; case "KICK": string kickChannelName = parameters[0]; string kickUserName = parameters[1]; connectionManager.OnUserKicked(kickChannelName, kickUserName); break; case "ERROR": connectionManager.OnErrorReceived(message); break; case "PING": if (parameters.Count > 0) { QueueMessage(new QueuedMessage("PONG " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)); Logger.Log("PONG " + parameters[0]); } else { QueueMessage(new QueuedMessage("PONG", QueuedMessageType.SYSTEM_MESSAGE, 5000)); Logger.Log("PONG"); } break; case "TOPIC": if (parameters.Count < 2) break; connectionManager.OnChannelTopicChanged(prefix.Substring(0, prefix.IndexOf('!')), parameters[0], parameters[1]); break; case "NICK": int nickExclamIndex = prefix.IndexOf('!'); if (nickExclamIndex > -1 || parameters.Count < 1) { string oldNick = prefix.Substring(0, nickExclamIndex); string newNick = parameters[0]; Logger.Log("Nick change - " + oldNick + " -> " + newNick); connectionManager.OnUserNicknameChange(oldNick, newNick); } break; } } catch { Logger.Log("Warning: Failed to parse command " + message); } } private string GetIdentFromPrefix(string prefix) { int atIndex = prefix.IndexOf('@'); int exclamIndex = prefix.IndexOf('!'); if (exclamIndex == -1 || atIndex == -1) return string.Empty; return prefix.Substring(exclamIndex + 1, atIndex - (exclamIndex + 1)); } /// /// Parses a single IRC message received from the server. /// /// The message. /// (out) The message prefix. /// (out) The command. /// (out) The parameters of the command. private void ParseIrcMessage(string message, out string prefix, out string command, out List parameters) { int prefixEnd = -1; prefix = command = String.Empty; parameters = new List(); // Grab the prefix if it is present. If a message begins // with a colon, the characters following the colon until // the first space are the prefix. if (message.StartsWith(":")) { prefixEnd = message.IndexOf(" "); prefix = message.Substring(1, prefixEnd - 1); } // Grab the trailing if it is present. If a message contains // a space immediately following a colon, all characters after // the colon are the trailing part. int trailingStart = message.IndexOf(" :"); string trailing = null; if (trailingStart >= 0) trailing = message.Substring(trailingStart + 2); else trailingStart = message.Length; // Use the prefix end position and trailing part start // position to extract the command and parameters. var commandAndParameters = message.Substring(prefixEnd + 1, trailingStart - prefixEnd - 1).Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (commandAndParameters.Length == 0) { command = String.Empty; Logger.Log("Nonexistant command!"); return; } // The command will always be the first element of the array. command = commandAndParameters[0]; // The rest of the elements are the parameters, if they exist. // Skip the first element because that is the command. if (commandAndParameters.Length > 1) { for (int id = 1; id < commandAndParameters.Length; id++) { parameters.Add(commandAndParameters[id]); } } // If the trailing part is valid add the trailing part to the // end of the parameters. if (!string.IsNullOrEmpty(trailing)) parameters.Add(trailing); } #endregion #region Sending commands private void RunSendQueue() { while (_isConnected) { string message = String.Empty; lock (messageQueueLocker) { for (int i = 0; i < MessageQueue.Count; i++) { QueuedMessage qm = MessageQueue[i]; if (qm.Delay > 0) { if (qm.SendAt < DateTime.Now) { message = qm.Command; Logger.Log("Delayed message sent: " + qm.ID); MessageQueue.RemoveAt(i); break; } } else { message = qm.Command; MessageQueue.RemoveAt(i); break; } } } if (String.IsNullOrEmpty(message)) { Thread.Sleep(10); continue; } SendMessage(message); Thread.Sleep(MessageQueueDelay); } lock (messageQueueLocker) { MessageQueue.Clear(); } sendQueueExited = true; } /// /// Sends a PING message to the server to indicate that we're still connected. /// /// Just a dummy parameter so that this matches the delegate System.Threading.TimerCallback. private void AutoPing(object data) { SendMessage("PING LAG" + Rng.Next(100000, 999999)); } /// /// Registers the user. /// private void Register() { if (welcomeMessageReceived) return; Logger.Log("Registering."); var defaultGame = ClientConfiguration.Instance.LocalGame; string realname = ProgramConstants.GAME_VERSION + " " + defaultGame + " CnCNet"; SendMessage(string.Format("USER {0} 0 * :{1}", defaultGame + "." + systemId, realname)); SendMessage("NICK " + ProgramConstants.PLAYERNAME); } public void ChangeNickname() { SendMessage("NICK " + ProgramConstants.PLAYERNAME); } public void QueueMessage(QueuedMessageType type, int priority, string message, bool replace = false) { QueuedMessage qm = new QueuedMessage(message, type, priority, replace); QueueMessage(qm); } public void QueueMessage(QueuedMessageType type, int priority, int delay, string message) { QueuedMessage qm = new QueuedMessage(message, type, priority, delay); QueueMessage(qm); Logger.Log("Setting delay to " + delay + "ms for " + qm.ID); } /// /// Send a message to the CnCNet server. /// /// The message to send. private void SendMessage(string message) { if (serverStream == null) return; Logger.Log("SRM: " + message); byte[] buffer = encoding.GetBytes(message + "\r\n"); if (serverStream.CanWrite) { try { serverStream.Write(buffer, 0, buffer.Length); serverStream.Flush(); } catch (IOException ex) { Logger.Log("Sending message to the server failed! Reason: " + ex.ToString()); } } } private int NextQueueID { get; set; } = 0; /// /// This will attempt to replace a previously queued message of the same type. /// /// The new message to replace with /// Whether or not a replace occurred private bool ReplaceMessage(QueuedMessage qm) { lock (messageQueueLocker) { var previousMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType); if (previousMessageIndex == -1) return false; MessageQueue[previousMessageIndex] = qm; return true; } } /// /// Adds a message to the send queue. /// /// The message to queue. /// If true, attempt to replace a previous message of the same type public void QueueMessage(QueuedMessage qm) { if (!_isConnected) return; if (qm.Replace && ReplaceMessage(qm)) return; qm.ID = NextQueueID++; lock (messageQueueLocker) { switch (qm.MessageType) { case QueuedMessageType.GAME_BROADCASTING_MESSAGE: case QueuedMessageType.GAME_PLAYERS_MESSAGE: case QueuedMessageType.GAME_SETTINGS_MESSAGE: case QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE: case QueuedMessageType.GAME_LOCKED_MESSAGE: case QueuedMessageType.GAME_GET_READY_MESSAGE: case QueuedMessageType.GAME_NOTIFICATION_MESSAGE: case QueuedMessageType.GAME_HOSTING_MESSAGE: case QueuedMessageType.WHOIS_MESSAGE: case QueuedMessageType.GAME_CHEATER_MESSAGE: AddSpecialQueuedMessage(qm); break; case QueuedMessageType.INSTANT_MESSAGE: SendMessage(qm.Command); break; default: int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority); if (ProgramConstants.LOG_LEVEL > 1) Logger.Log("QM Undefined: " + qm.Command + " " + placeInQueue); if (placeInQueue == -1) MessageQueue.Add(qm); else MessageQueue.Insert(placeInQueue, qm); break; } } } /// /// Adds a "special" message to the send queue that replaces /// previous messages of the same type in the queue. /// /// The message to queue. private void AddSpecialQueuedMessage(QueuedMessage qm) { int broadcastingMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType); qm.ID = NextQueueID++; if (broadcastingMessageIndex > -1) { if (ProgramConstants.LOG_LEVEL > 1) Logger.Log("QM Replace: " + qm.Command + " " + broadcastingMessageIndex); MessageQueue[broadcastingMessageIndex] = qm; } else { int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority); if (ProgramConstants.LOG_LEVEL > 1) Logger.Log("QM: " + qm.Command + " " + placeInQueue); if (placeInQueue == -1) MessageQueue.Add(qm); else MessageQueue.Insert(placeInQueue, qm); } } #endregion } } ================================================ FILE: DXMainClient/Online/EventArguments/AttemptedServerEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { /// /// Event arguments for a server connection attempt. /// public class AttemptedServerEventArgs : EventArgs { public AttemptedServerEventArgs(string serverName) { ServerName = serverName; } public string ServerName { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/CTCPEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class CTCPEventArgs : EventArgs { public CTCPEventArgs(string sender, string channelName, string ctcpMessage) { Sender = sender; ChannelName = channelName; CTCPMessage = ctcpMessage; } public string Sender { get; private set; } public string ChannelName { get; private set; } public string CTCPMessage { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/ChannelCTCPEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class ChannelCTCPEventArgs : EventArgs { public ChannelCTCPEventArgs(string userName, string message) { UserName = userName; Message = message; } public string UserName { get; private set; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/ChannelEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class ChannelEventArgs : EventArgs { public ChannelEventArgs(string channelName) { ChannelName = channelName; } public string ChannelName { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/ChannelModeEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class ChannelModeEventArgs : EventArgs { public ChannelModeEventArgs(string userName, string modeString) { UserName = userName; ModeString = modeString; } public string UserName { get; set; } public string ModeString { get; set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/ChannelTopicEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class ChannelTopicEventArgs : EventArgs { public ChannelTopicEventArgs(string channelName, string topic) { ChannelName = channelName; Topic = topic; } public string ChannelName { get; private set; } public string Topic { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/CnCNetPrivateMessageEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class CnCNetPrivateMessageEventArgs : EventArgs { public CnCNetPrivateMessageEventArgs(string sender, string message) { Sender = sender; Message = message; DateTime = DateTime.Now; } public DateTime DateTime { get; set; } public string Sender { get; private set; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/ConnectionLostEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class ConnectionLostEventArgs : EventArgs { public ConnectionLostEventArgs(string reason) { Reason = reason; } public string Reason { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/FavoriteMapEventArgs.cs ================================================ using System; using DTAClient.Domain.Multiplayer; namespace DTAClient.Online.EventArguments { public class FavoriteMapEventArgs : EventArgs { public readonly Map Map; public FavoriteMapEventArgs(Map map) { Map = map; } } } ================================================ FILE: DXMainClient/Online/EventArguments/GameOptionPresetEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class GameOptionPresetEventArgs : EventArgs { public string PresetName { get; } public GameOptionPresetEventArgs(string presetName) { PresetName = presetName; } } } ================================================ FILE: DXMainClient/Online/EventArguments/JoinUserEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class JoinUserEventArgs : EventArgs { public IRCUser IrcUser { get; } public JoinUserEventArgs(IRCUser ircUser) { IrcUser = ircUser; } } } ================================================ FILE: DXMainClient/Online/EventArguments/KickEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class KickEventArgs : EventArgs { public KickEventArgs(string channelName, string userName) { ChannelName = channelName; UserName = userName; } public string ChannelName { get; private set; } public string UserName { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/MultiplayerNameRightClickedEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class MultiplayerNameRightClickedEventArgs : EventArgs { public string PlayerName { get; } public MultiplayerNameRightClickedEventArgs(string playerName) { PlayerName = playerName; } } } ================================================ FILE: DXMainClient/Online/EventArguments/PrivateCTCPEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class PrivateCTCPEventArgs : EventArgs { public PrivateCTCPEventArgs(string sender, string message) { Sender = sender; Message = message; } public string Sender { get; private set; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/PrivateMessageEventArgs.cs ================================================ namespace DTAClient.Online.EventArguments { public class PrivateMessageEventArgs : CnCNetPrivateMessageEventArgs { public readonly IRCUser ircUser; public PrivateMessageEventArgs(string sender, string message, IRCUser ircUser) : base(sender, message) { this.ircUser = ircUser; } } } ================================================ FILE: DXMainClient/Online/EventArguments/ServerMessageEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { /// /// Generic event argument class for a IRC server message. /// public class ServerMessageEventArgs : EventArgs { public ServerMessageEventArgs(string message) { Message = message; } public string Message { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/UnreadMessageCountEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class UnreadMessageCountEventArgs : EventArgs { public int UnreadMessageCount { get; set; } public UnreadMessageCountEventArgs(int unreadMessageCount) { UnreadMessageCount = unreadMessageCount; } } } ================================================ FILE: DXMainClient/Online/EventArguments/UserAwayEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class UserAwayEventArgs : EventArgs { public UserAwayEventArgs(string user, string awayReason) { UserName = user; AwayReason = awayReason; } public string UserName { get; private set; } public string AwayReason { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/UserListEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class UserListEventArgs : EventArgs { public UserListEventArgs(string channelName, string[] userNames) { ChannelName = channelName; UserNames = userNames; } public string ChannelName { get; private set; } public string[] UserNames { get; private set; } } } ================================================ FILE: DXMainClient/Online/EventArguments/WhoEventArgs.cs ================================================ using System; namespace DTAClient.Online.EventArguments { public class WhoEventArgs : EventArgs { public WhoEventArgs(string ident, string userName, string extraInfo) { Ident = ident; UserName = userName; ExtraInfo = extraInfo; } public string Ident { get; private set; } public string UserName { get; private set; } public string ExtraInfo { get; private set; } } } ================================================ FILE: DXMainClient/Online/FileHashCalculator.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using ClientCore; using ClientCore.I18N; using ClientCore.Enums; using Rampastring.Tools; namespace DTAClient.Online { public class FileHashCalculator { private const string CONFIGNAME = "FHCConfig.ini"; private bool calculateGameExeHash = true; private static readonly IReadOnlyList knownTextFileExtensions = [".txt", ".ini", ".json", ".xml"]; private string[] fileNamesToCheck = ClientConfiguration.Instance.ClientGameType switch { ClientType.TS => new string[] { "spawner.xdp", "rules.ini", "ai.ini", "art.ini", "shroud.shp", "INI/Rules.ini", "INI/Enhance.ini", "INI/Firestrm.ini", "INI/Art.ini", "INI/ArtE.ini", "INI/ArtFS.ini", "INI/AI.ini", "INI/AIE.ini", "INI/AIFS.ini" }, ClientType.YR => new string[] { "spawner.xdp", "spawner2.xdp", "artmd.ini", "soundmd.ini", "aimd.ini", "shroud.shp", "INI/Map Code/Cooperative.ini", "INI/Map Code/Free For All.ini", "INI/Map Code/Land Rush.ini", "INI/Map Code/Meat Grinder.ini", "INI/Map Code/Megawealth.ini", "INI/Map Code/Naval War.ini", "INI/Map Code/Standard.ini", "INI/Map Code/Team Alliance.ini", "INI/Map Code/Unholy Alliance.ini", "INI/Game Options/Allies Allowed.ini", "INI/Game Options/Brutal AI.ini", "INI/Game Options/No Dog Engi Eat.ini", "INI/Game Options/No Spawn Previews.ini", "INI/Game Options/RA2 Classic Mode.ini", "INI/Map Code/GlobalCode.ini", "INI/Map Code/MultiplayerGlobalCode.ini" }, ClientType.Ares => new string[] { "Ares.dll", "Ares.dll.inj", "Ares.mix", "Syringe.exe", "cncnet5.dll", "rulesmd.ini", "artmd.ini", "soundmd.ini", "aimd.ini", "shroud.shp" }, _ => new string[] { } }; public FileHashCalculator() => ParseConfigFile(); private string finalHash = string.Empty; public void CalculateHashes() { FileHashes fh = new() { ClientDefinitionsHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), ClientConfiguration.CLIENT_DEFS)), GameOptionsHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, ClientConfiguration.GAME_OPTIONS)), ClientDXHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "clientdx.exe")), ClientXNAHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "clientxna.exe")), ClientOGLHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "clientogl.exe")), ClientDXNET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "BinariesNET8", "Windows", "clientdx.dll")), ClientXNANET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "BinariesNET8", "XNA", "clientxna.dll")), ClientOGLNET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "BinariesNET8", "OpenGL", "clientogl.dll")), ClientUGLNET8Hash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), "BinariesNET8", "UniversalGL", "clientogl.dll")), GameExeHash = calculateGameExeHash ? CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.GetGameExecutableName())) : string.Empty, LauncherExeHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.GameLauncherExecutableName)), MPMapsHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath)), FHCConfigHash = CalculateSHA1ForFile(SafePath.CombineFilePath(ProgramConstants.BASE_RESOURCE_PATH, CONFIGNAME)), }; Logger.Log($"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\{ClientConfiguration.CLIENT_DEFS}: {fh.ClientDefinitionsHash}"); Logger.Log($"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\{CONFIGNAME}: {fh.FHCConfigHash}"); Logger.Log($"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\{ClientConfiguration.GAME_OPTIONS}: {fh.GameOptionsHash}"); Logger.Log($"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\clientdx.exe: {fh.ClientDXHash}"); Logger.Log($"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\clientxna.exe: {fh.ClientXNAHash}"); Logger.Log($"Hash for {ProgramConstants.BASE_RESOURCE_PATH}\\clientogl.exe: {fh.ClientOGLHash}"); Logger.Log($"Hash for ClientDX NET8: {fh.ClientDXNET8Hash}"); Logger.Log($"Hash for ClientXNA NET8: {fh.ClientXNANET8Hash}"); Logger.Log($"Hash for ClientOGL NET8: {fh.ClientOGLNET8Hash}"); Logger.Log($"Hash for ClientUGL NET8: {fh.ClientUGLNET8Hash}"); Logger.Log($"Hash for {ClientConfiguration.Instance.MPMapsIniPath}: {fh.MPMapsHash}"); if (calculateGameExeHash) Logger.Log($"Hash for {ClientConfiguration.Instance.GetGameExecutableName()}: {fh.GameExeHash}"); if (!string.IsNullOrEmpty(ClientConfiguration.Instance.GameLauncherExecutableName)) Logger.Log($"Hash for {ClientConfiguration.Instance.GameLauncherExecutableName}: {fh.LauncherExeHash}"); foreach (string relativePath in fileNamesToCheck) { string fullPath = SafePath.CombineFilePath(ProgramConstants.GamePath, relativePath); string hash = fh.AddHashForFileIfExists(relativePath, fullPath); if (!string.IsNullOrEmpty(hash)) Logger.Log($"Hash for {relativePath}: {hash}"); } List iniPaths = [SafePath.GetDirectory(ProgramConstants.GamePath, "INI", "Game Options")]; if (ClientConfiguration.Instance.ClientGameType != ClientType.YR) iniPaths.Add(SafePath.GetDirectory(ProgramConstants.GamePath, "INI", "Map Code")); foreach (DirectoryInfo path in iniPaths) { if (path.Exists) { foreach (string filename in path.EnumerateFiles("*", SearchOption.AllDirectories).Select(s => s.FullName.Substring(path.FullName.Length))) { string fileRelativePath = SafePath.CombineFilePath(path.Name, filename); string fileFullPath = SafePath.CombineFilePath(path.FullName, filename); Debug.Assert(File.Exists(fileFullPath), $"File {fileFullPath} is supposed to but does not exist."); string hash = fh.AddHashForFileIfExists(fileRelativePath, fileFullPath); if (!string.IsNullOrEmpty(hash)) Logger.Log("Hash for " + fileRelativePath + ": " + hash); } } } // Add the hashes for each checked file from the available translations if (Directory.Exists(ClientConfiguration.Instance.TranslationsFolderPath)) { DirectoryInfo translationsFolderPath = SafePath.GetDirectory(ClientConfiguration.Instance.TranslationsFolderPath); List translationGameFiles = ClientConfiguration.Instance.TranslationGameFiles .Where(tgf => tgf.Checked).ToList(); foreach (DirectoryInfo translationFolder in translationsFolderPath.EnumerateDirectories()) { foreach (TranslationGameFile tgf in translationGameFiles) { string fileRelativePath = SafePath.CombineFilePath(translationFolder.Name, tgf.Source); string fileFullPath = SafePath.CombineFilePath(translationFolder.FullName, tgf.Source); string hash = fh.AddHashForFileIfExists(fileRelativePath, fileFullPath); if (!string.IsNullOrEmpty(hash)) Logger.Log($"Hash for {fileRelativePath}: {hash}"); } } } finalHash = fh.GetFinalHash(); Logger.Log($"Complete hash: {finalHash}"); } public string GetCompleteHash() => finalHash; private void ParseConfigFile() { IniFile config = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(), CONFIGNAME)); calculateGameExeHash = config.GetBooleanValue("Settings", "CalculateGameExeHash", true); List keys = config.GetSectionKeys("FilenameList"); if (keys == null || keys.Count < 1) return; List filenames = new List(); foreach (string key in keys) { string value = config.GetStringValue("FilenameList", key, string.Empty); filenames.Add(value == string.Empty ? key : value); } fileNamesToCheck = filenames.ToArray(); } private static string NormalizePath(string path) => path.Replace('\\', '/'); private static string CalculateSHA1ForFile(string path) { if (string.IsNullOrWhiteSpace(path)) return string.Empty; FileInfo file = SafePath.GetFile(path); if (!file.Exists) return string.Empty; using Stream inputStream = file.OpenRead(); if (knownTextFileExtensions.Contains(file.Extension, StringComparer.InvariantCultureIgnoreCase)) { // Normalize line endings to LF UTF8Encoding utf8Encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); using StreamReader reader = new(inputStream, utf8Encoding, detectEncodingFromByteOrderMarks: false); string text = reader.ReadToEnd(); text = text.Replace("\r\n", "\n").Trim(); byte[] bytes = utf8Encoding.GetBytes(text); using SHA1 sha1 = SHA1.Create(); return BytesToString(sha1.ComputeHash(bytes)); } else { using SHA1 sha1 = SHA1.Create(); return BytesToString(sha1.ComputeHash(inputStream)); } } private static string BytesToString(byte[] bytes) { char[] result = new char[bytes.Length * 2]; for (int i = 0; i < bytes.Length; i++) { byte b = bytes[i]; result[i * 2] = GetHexChar(b >> 4); result[i * 2 + 1] = GetHexChar(b & 0x0F); } return new string(result); } private static char GetHexChar(int digit) { Debug.Assert(digit >= 0 && digit < 16, $"Value {digit} is out of range for a hex digit."); return (char)(digit < 10 ? '0' + digit : 'a' + digit - 10); } private class FileHashes() { public string ClientDefinitionsHash; public string GameOptionsHash; public string ClientDXHash; public string ClientXNAHash; public string ClientOGLHash; public string ClientDXNET8Hash; public string ClientXNANET8Hash; public string ClientOGLNET8Hash; public string ClientUGLNET8Hash; public string MPMapsHash; public string GameExeHash; public string LauncherExeHash; public string FHCConfigHash; public readonly SortedDictionary AdditionalFileHashes = new(StringComparer.InvariantCultureIgnoreCase); public string AddHashForFileIfExists(string relativePath) => AddHashForFileIfExists(relativePath, relativePath); public string AddHashForFileIfExists(string relativePath, string filePath) { Debug.Assert(!relativePath.StartsWith(ProgramConstants.GamePath), $"File path {relativePath} should be a relative path."); string hash = CalculateSHA1ForFile(filePath); if (!string.IsNullOrEmpty(hash)) { AdditionalFileHashes[NormalizePath(relativePath)] = hash; return hash; } else { return string.Empty; } } public string GetFinalHash() { var sb = new StringBuilder(); sb.Append(ClientDefinitionsHash); sb.Append(GameOptionsHash); sb.Append(ClientDXHash); sb.Append(ClientXNAHash); sb.Append(ClientOGLHash); sb.Append(ClientDXNET8Hash); sb.Append(ClientXNANET8Hash); sb.Append(ClientOGLNET8Hash); sb.Append(ClientUGLNET8Hash); sb.Append(GameExeHash); sb.Append(LauncherExeHash); sb.Append(MPMapsHash); sb.Append(FHCConfigHash); // Append additional file hashes, ordered by key foreach (string fileHash in AdditionalFileHashes.Values) sb.Append(fileHash); // Merge hashes string finalHash = sb.ToString(); byte[] buffer = Encoding.ASCII.GetBytes(finalHash); using SHA1 sha1 = SHA1.Create(); byte[] hash = sha1.ComputeHash(buffer); return BytesToString(hash); } } } } ================================================ FILE: DXMainClient/Online/IConnectionManager.cs ================================================ using System.Collections.Generic; namespace DTAClient.Online { /// /// An interface for handling IRC messages. /// public interface IConnectionManager { void OnWelcomeMessageReceived(string message); void OnGenericServerMessageReceived(string message); void OnAwayMessageReceived(string userName, string reason); void OnChannelTopicReceived(string channelName, string topic); void OnChannelTopicChanged(string userName, string channelName, string topic); void OnUserListReceived(string channelName, string[] userList); void OnWhoReplyReceived(string ident, string hostName, string userName, string extraInfo); void OnChannelFull(string channelName); void OnTargetChangeTooFast(string channelName, string message); void OnChannelInviteOnly(string channelName); void OnIncorrectChannelPassword(string channelName); void OnCTCPParsed(string channelName, string userName, string message); void OnNoticeMessageParsed(string notice, string userName); void OnUserJoinedChannel(string channelName, string hostName, string userName, string ident); void OnUserLeftChannel(string channelName, string userName); void OnUserQuitIRC(string userName); void OnChatMessageReceived(string receiver, string senderName, string senderIdent, string message); void OnPrivateMessageReceived(string sender, string message); void OnChannelModesChanged(string userName, string channelName, string modeString, List modeParameters); void OnUserKicked(string channelName, string userName); void OnErrorReceived(string errorMessage); void OnNameAlreadyInUse(); void OnBannedFromChannel(string channelName); void OnUserNicknameChange(string oldNickname, string newNickname); // ********************** // Connection-related methods // ********************** void OnAttemptedServerChanged(string serverName); void OnConnectAttemptFailed(); void OnConnectionLost(string reason); void OnReconnectAttempt(); void OnDisconnected(); void OnConnected(); bool GetDisconnectStatus(); void OnServerLatencyTested(int candidateCount, int closerCount); //public EventHandler WelcomeMessageReceived; //public EventHandler GenericServerMessageReceived; //public EventHandler AwayMessageReceived; //public EventHandler ChannelTopicReceived; //public EventHandler UserListReceived; //public EventHandler WhoReplyReceived; //public EventHandler ChannelFull; //public EventHandler IncorrectChannelPassword; //public event EventHandler AttemptedServerChanged; //public event EventHandler ConnectAttemptFailed; //public event EventHandler ConnectionLost; //public event EventHandler ReconnectAttempt; } } ================================================ FILE: DXMainClient/Online/IRCColor.cs ================================================ using Microsoft.Xna.Framework; namespace DTAClient.Online { public class IRCColor { public IRCColor(string name, bool selectable, Color xnaColor, int ircColorId) { Name = name; Selectable = selectable; XnaColor = xnaColor; IrcColorId = ircColorId; } public string Name { get; private set; } public bool Selectable { get; private set; } public Color XnaColor { get; private set; } public int IrcColorId { get; private set; } } } ================================================ FILE: DXMainClient/Online/IRCUser.cs ================================================ using System; using System.Collections.Generic; namespace DTAClient.Online { /// /// A user on an IRC server. /// public class IRCUser : ICloneable { public IRCUser() { } public IRCUser(string name) { Name = name; } public IRCUser(string name, string ident, string host) { Name = name; Ident = ident; Hostname = host; } public string Name { get; set; } public string Ident { get; set; } public string Hostname { get; set; } public int GameID { get; set; } = -1; public List Channels = new List(); public object Clone() { return MemberwiseClone(); } public bool IsFriend { get; set; } public bool IsIgnored { get; set; } public bool HasVoice { get; set; } } } ================================================ FILE: DXMainClient/Online/IUserCollection.cs ================================================ using System; using System.Collections.Generic; namespace DTAClient.Online { public interface IUserCollection { int Count { get; } void Add(string username, T item); void Clear(); void DoForAllUsers(Action action); T Find(string username); LinkedListNode GetFirst(); void Reinsert(string username); bool Remove(string username); } } ================================================ FILE: DXMainClient/Online/PrivateMessageHandler.cs ================================================ using System; using DTAClient.Online.EventArguments; namespace DTAClient.Online { /// /// This is responsible for handling the receiving of private messages from CnCNet and performing any logic checks /// as to whether the message should be ignored, independent from any GUI. This will then forward valid private message /// events to other consumers. /// public class PrivateMessageHandler { private readonly CnCNetUserData _cncnetUserData; private readonly CnCNetManager _connectionManager; private int UnreadMessageCount; public event EventHandler PrivateMessageReceived; public event EventHandler UnreadMessageCountUpdated; public PrivateMessageHandler( CnCNetManager connectionManager, CnCNetUserData cncnetUserData ) { _connectionManager = connectionManager; _cncnetUserData = cncnetUserData; _connectionManager.PrivateMessageReceived += _PrivateMessageReceived; } private void _PrivateMessageReceived(object sender, CnCNetPrivateMessageEventArgs e) { IRCUser iu = _connectionManager.UserList.Find(u => u.Name == e.Sender); // We don't accept PMs from people who we don't share any channels with if (iu == null) return; // Messages from users we've blocked are not wanted if (_cncnetUserData.IsIgnored(iu.Ident)) return; var privateMessageEventArgs = new PrivateMessageEventArgs(e.Sender, e.Message, iu); PrivateMessageReceived?.Invoke(this, privateMessageEventArgs); } private void DoUnreadMessageCountUpdated() => UnreadMessageCountUpdated?.Invoke(this, new UnreadMessageCountEventArgs(UnreadMessageCount)); private void SetUnreadMessageCount(int unreadMessageCount) { UnreadMessageCount = unreadMessageCount; DoUnreadMessageCountUpdated(); } /// /// This can be called by specific GUI components to trigger than any unread counts should be reset, /// because the PrivateMessageWindow was made visible. /// public void ResetUnreadMessageCount() => SetUnreadMessageCount(0); /// /// This can be called by specific GUI components to trigger than any unread counts should be incremented, /// because the PrivateMessageWindow may not currently be visible. /// public void IncrementUnreadMessageCount() => SetUnreadMessageCount(UnreadMessageCount + 1); } } ================================================ FILE: DXMainClient/Online/PrivateMessageUser.cs ================================================ using System.Collections.Generic; namespace DTAClient.Online { class PrivateMessageUser { public PrivateMessageUser(IRCUser user) { IrcUser = user; } public IRCUser IrcUser { get; private set; } public List Messages = new List(); } } ================================================ FILE: DXMainClient/Online/QueuedMessage.cs ================================================ using System; namespace DTAClient.Online { /// /// A queued network message. /// public class QueuedMessage { private const int DEFAULT_DELAY = -1; private const int REPLACE_DELAY = 1; public QueuedMessage(string command, QueuedMessageType type, int priority) : this(command, type, priority, DEFAULT_DELAY, false) { } public QueuedMessage(string command, QueuedMessageType type, int priority, bool replace) : this(command, type, priority, replace ? REPLACE_DELAY : DEFAULT_DELAY, replace) { } public QueuedMessage(string command, QueuedMessageType type, int priority, int delay) : this(command, type, priority, delay, false) { } private QueuedMessage(string command, QueuedMessageType type, int priority, int delay, bool replace) { Command = command; MessageType = type; Priority = priority; Delay = delay; SendAt = Delay < 0 ? DateTime.Now : DateTime.Now.AddMilliseconds(Delay); Replace = replace; } /// /// Message Queue ID /// public int ID { get; set; } /// /// The command to send to the IRC network. /// public string Command { get; set; } /// /// The type of the message. /// public QueuedMessageType MessageType { get; set; } /// /// The priority of the message. /// public int Priority { get; set; } /// /// The amount of milliseconds to delay the message. /// public int Delay { get; set; } /// /// The amount of milliseconds to delay the message. /// public DateTime SendAt { get; set; } /// /// This can be used to replace a message on the queue to help prevent flooding purposes. /// This should be used with at least a small delay. /// public bool Replace { get; set; } = false; } } ================================================ FILE: DXMainClient/Online/QueuedMessageType.cs ================================================ namespace DTAClient.Online { /// /// The type of a CnCNet IRC network message. /// public enum QueuedMessageType { UNDEFINED, CHAT_MESSAGE, SYSTEM_MESSAGE, GAME_SETTINGS_MESSAGE, GAME_PLAYERS_MESSAGE, GAME_PLAYERS_READY_STATUS_MESSAGE, GAME_LOCKED_MESSAGE, GAME_GET_READY_MESSAGE, GAME_NOTIFICATION_MESSAGE, GAME_HOSTING_MESSAGE, GAME_CHEATER_MESSAGE, GAME_BROADCASTING_MESSAGE, WHOIS_MESSAGE, INSTANT_MESSAGE, GAME_PLAYERS_EXTRA_MESSAGE, } } ================================================ FILE: DXMainClient/Online/RecentPlayer.cs ================================================ using System; using System.Text.Json.Serialization; namespace DTAClient.Online { public class RecentPlayer { [JsonInclude] public string PlayerName { get; set; } [JsonInclude] public string GameName { get; set; } [JsonInclude] public DateTime GameTime { get; set; } } } ================================================ FILE: DXMainClient/Online/Server.cs ================================================ using System; namespace DTAClient.Online { /// /// A struct containing information on an IRC server. /// public struct Server { public Server(string host, string name, int[] ports) { Host = host; Name = name; Ports = ports; } public string Host; public string Name; public int[] Ports; public string Serialize() => FormattableString.Invariant($"{Host}|{Name}|{string.Join(",", Ports)}"); public static Server Deserialize(string serialized) { string[] parts = serialized.Split('|'); string host = parts[0]; string name = parts[1]; string[] portStrings = parts[2].Split(','); int[] ports = new int[portStrings.Length]; for (int i = 0; i < portStrings.Length; i++) ports[i] = int.Parse(portStrings[i]); return new Server(host, name, ports); } } } ================================================ FILE: DXMainClient/Online/SortedUserCollection.cs ================================================ using System; using System.Collections.Generic; namespace DTAClient.Online { /// /// A custom collection that aims to provide quick insertion, /// removal and lookup operations while always keeping the list sorted /// by combining Dictionary and LinkedList. /// public class SortedUserCollection : IUserCollection { public SortedUserCollection(Func userComparer) { dictionary = new Dictionary>(); linkedList = new LinkedList(); this.userComparer = userComparer; } private readonly Dictionary> dictionary; private readonly LinkedList linkedList; private readonly Func userComparer; public int Count => dictionary.Count; public void Add(string username, T item) { if (linkedList.Count == 0) { var node = linkedList.AddFirst(item); dictionary.Add(username.ToLower(), node); return; } var currentNode = linkedList.First; while (true) { if (userComparer(currentNode.Value, item) > 0) { var node = linkedList.AddBefore(currentNode, item); dictionary.Add(username.ToLower(), node); break; } if (currentNode.Next == null) { var node = linkedList.AddAfter(currentNode, item); dictionary.Add(username.ToLower(), node); break; } currentNode = currentNode.Next; } } public bool Remove(string username) { if (dictionary.TryGetValue(username.ToLower(), out var node)) { linkedList.Remove(node); dictionary.Remove(username.ToLower()); return true; } return false; } public T Find(string username) { if (dictionary.TryGetValue(username.ToLower(), out var node)) return node.Value; return default(T); } public void Reinsert(string username) { var existing = Find(username.ToLower()); if (existing == null) return; Remove(username); Add(username, existing); } public void Clear() { linkedList.Clear(); dictionary.Clear(); } public LinkedListNode GetFirst() => linkedList.First; public void DoForAllUsers(Action action) { var current = linkedList.First; while (current != null) { action(current.Value); current = current.Next; } } } } ================================================ FILE: DXMainClient/Online/UnsortedUserCollection.cs ================================================ using System; using System.Collections.Generic; namespace DTAClient.Online { /// /// A custom collection that aims to provide quick insertion, /// removal and lookup operations by using a dictionary. Does not /// keep the list sorted. /// public class UnsortedUserCollection : IUserCollection { private Dictionary dictionary = new Dictionary(); public int Count => dictionary.Count; public void Add(string username, T item) { dictionary.Add(username.ToLower(), item); } public void Clear() { dictionary.Clear(); } public void DoForAllUsers(Action action) { var values = dictionary.Values; foreach (T value in values) { action(value); } } public T Find(string username) { if (dictionary.TryGetValue(username.ToLower(), out T value)) return value; return default(T); } LinkedListNode IUserCollection.GetFirst() { throw new NotImplementedException(); } void IUserCollection.Reinsert(string username) { throw new NotImplementedException(); } public bool Remove(string username) { return dictionary.Remove(username.ToLower()); } } } ================================================ FILE: DXMainClient/PreStartup.cs ================================================ using System; #if WINFORMS using System.Windows.Forms; #endif using System.Diagnostics; using System.IO; using System.Threading.Tasks; using DTAClient.Domain; using Rampastring.Tools; using ClientCore; using System.Security.AccessControl; using System.Security.Principal; using System.Collections.Generic; using ClientCore.Extensions; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; using ClientCore.I18N; using System.Globalization; using System.Security; using System.Transactions; using DTAClient.DXGUI.Multiplayer.GameLobby; namespace DTAClient { /// /// Contains client startup parameters. /// struct StartupParams { public StartupParams(bool noAudio, bool multipleInstanceMode, List unknownParams) { NoAudio = noAudio; MultipleInstanceMode = multipleInstanceMode; UnknownStartupParams = unknownParams; } public bool NoAudio { get; } public bool MultipleInstanceMode { get; } public List UnknownStartupParams { get; } } static class PreStartup { private static readonly Stopwatch startupStopwatch = Stopwatch.StartNew(); public static TimeSpan StartupElapsed => startupStopwatch.Elapsed; /// /// Initializes various basic systems like the client's logger, /// constants, and the general exception handler. /// Reads the user's settings from an INI file, /// checks for necessary permissions and starts the client if /// everything goes as it should. /// /// The client's startup parameters. public static void Initialize(StartupParams parameters) { Translation.InitialUICulture = CultureInfo.CurrentUICulture; CultureInfo.CurrentUICulture = new CultureInfo(ProgramConstants.HARDCODED_LOCALE_CODE); #if WINFORMS Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException); Application.ThreadException += (sender, args) => HandleException(sender, args.Exception); #endif AppDomain.CurrentDomain.UnhandledException += (sender, args) => HandleException(sender, (Exception)args.ExceptionObject); DirectoryInfo gameDirectory = SafePath.GetDirectory(ProgramConstants.GamePath); Environment.CurrentDirectory = gameDirectory.FullName; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) CheckPermissions(); DirectoryInfo clientUserFilesDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); FileInfo clientLogFile = SafePath.GetFile(clientUserFilesDirectory.FullName, "client.log"); ProgramConstants.LogFileName = clientLogFile.FullName; if (clientLogFile.Exists) { // Copy client.log file as client_previous.log. Override client_previous.log if it exists. FileInfo clientPrevLogFile = SafePath.GetFile(clientUserFilesDirectory.FullName, "client_previous.log"); if (clientPrevLogFile.Exists) File.Delete(clientPrevLogFile.FullName); File.Move(clientLogFile.FullName, clientPrevLogFile.FullName); } Logger.Initialize(clientUserFilesDirectory.FullName, clientLogFile.Name); Logger.WriteLogFile = true; MainClientConstants.LoggerInitialized = true; if (!clientUserFilesDirectory.Exists) clientUserFilesDirectory.Create(); Logger.Log("***Logfile for " + MainClientConstants.GAME_NAME_LONG + " client***"); string clientVersion = GitVersionInformation.AssemblySemVer; #if DEVELOPMENT_BUILD clientVersion = $"{GitVersionInformation.CommitDate} {GitVersionInformation.BranchName}@{GitVersionInformation.ShortSha}"; #endif Logger.Log($"Client version: {clientVersion}"); Logger.Log(GitVersionInformation.InformationalVersion); #if DEVELOPMENT_BUILD Logger.Log("This is a development build of the client. Stability and reliability may not be fully guaranteed."); #endif MainClientConstants.Initialize(); // Log information about given startup params if (parameters.NoAudio) { Logger.Log("Startup parameter: No audio"); // TODO fix throw new NotImplementedException("-NOAUDIO is currently not implemented, please run the client without it.".L10N("Client:Main:NoAudio")); } if (parameters.MultipleInstanceMode) Logger.Log("Startup parameter: Allow multiple client instances"); parameters.UnknownStartupParams.ForEach(p => Logger.Log("Unknown startup parameter: " + p)); Logger.Log("Loading settings."); UserINISettings.Initialize(ClientConfiguration.Instance.SettingsIniName); // Try to load translation try { Translation translation; FileInfo translationThemeFile = SafePath.GetFile(UserINISettings.Instance.TranslationThemeFolderPath, ClientConfiguration.Instance.TranslationIniName); FileInfo translationFile = SafePath.GetFile(UserINISettings.Instance.TranslationFolderPath, ClientConfiguration.Instance.TranslationIniName); if (translationFile.Exists) { Logger.Log($"Loading generic translation file at {translationFile.FullName}"); translation = new Translation(translationFile.FullName, UserINISettings.Instance.Translation); if (translationThemeFile.Exists) { Logger.Log($"Loading theme-specific translation file at {translationThemeFile.FullName}"); translation.AppendValuesFromIniFile(translationThemeFile.FullName); } Translation.Instance = translation; } else { Logger.Log($"Failed to load a translation file. " + $"Neither {translationThemeFile.FullName} nor {translationFile.FullName} exist."); } Logger.Log("Loaded translation: " + Translation.Instance.Name); } catch (Exception ex) { Logger.Log("Failed to load the translation file. " + ex.ToString()); Translation.Instance = new Translation(UserINISettings.Instance.Translation); } CultureInfo.CurrentUICulture = Translation.Instance.Culture; try { if (UserINISettings.Instance.GenerateTranslationStub) { string stubPath = SafePath.CombineFilePath( ProgramConstants.ClientUserFilesPath, ClientConfiguration.Instance.TranslationIniName); AppDomain.CurrentDomain.ProcessExit += (sender, e) => { Logger.Log("Writing the translation stub file."); var ini = Translation.Instance.DumpIni(UserINISettings.Instance.GenerateOnlyNewValuesInTranslationStub); ini.WriteIniFile(stubPath); }; Logger.Log("Translation stub generation feature is now enabled. The stub file will be written when the client exits."); // Lookup all compile-time available strings ClientCore.Generated.TranslationNotifier.Register(); ClientGUI.Generated.TranslationNotifier.Register(); ClientUpdater.Generated.TranslationNotifier.Register(); DTAClient.Generated.TranslationNotifier.Register(); } } catch (Exception ex) { Logger.Log("Failed to generate the translation stub: " + ex.ToString()); } // Custom mission initialization CustomMissionHelper.Initialize(); CustomMissionHelper.DeleteSupplementalMissionFiles(); // Delete obsolete files from old target project versions Task.Run(() => { gameDirectory.EnumerateFiles("mainclient.log").SingleOrDefault()?.Delete(); gameDirectory.EnumerateFiles("aunchupdt.dat").SingleOrDefault()?.Delete(); try { gameDirectory.EnumerateFiles("wsock32.dll").SingleOrDefault()?.Delete(); } catch (Exception ex) { LogException(ex); string error = ("Deleting wsock32.dll failed! Please close any " + "applications that could be using the file, and then start the client again." + "\n\n" + "Message:").L10N("Client:Main:DeleteWsock32Failed") + " " + ex.Message; MainClientConstants.DisplayErrorAction(null, error, true); } }); Startup startup = new(); #if DEBUG startup.Execute(); #else try { startup.Execute(); } catch (Exception ex) { // MainClientConstants.DisplayErrorAction might have been overriden by XNA messagebox, which might be unable to display an error message. // Fallback to MessageBox. MainClientConstants.DisplayErrorAction = MainClientConstants.DefaultDisplayErrorAction; HandleException(startup, ex); } #endif } public static void LogException(Exception ex, bool innerException = false) { if (!innerException) Logger.Log("KABOOOOOOM!!! Info:"); else Logger.Log("InnerException info:"); Logger.Log("Type: " + ex.GetType()); Logger.Log("Message: " + ex.Message); Logger.Log("Source: " + ex.Source); Logger.Log("TargetSite.Name: " + ex.TargetSite?.Name); Logger.Log("Stacktrace: " + ex.StackTrace); if (ex.InnerException is not null) LogException(ex.InnerException, true); } public static void HandleException(object sender, Exception ex) { LogException(ex, innerException: false); string errorLogPath = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "ClientCrashLogs", FormattableString.Invariant($"ClientCrashLog{DateTime.Now.ToString("_yyyy_MM_dd_HH_mm")}.txt")); bool crashLogCopied = false; try { DirectoryInfo crashLogsDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, "ClientCrashLogs"); if (!crashLogsDirectoryInfo.Exists) crashLogsDirectoryInfo.Create(); File.Copy(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "client.log"), errorLogPath, true); crashLogCopied = true; } catch { } string error = string.Format("{0} has crashed. Error message:".L10N("Client:Main:FatalErrorText1") + Environment.NewLine + Environment.NewLine + ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ? "A crash log has been saved to the following file:".L10N("Client:Main:FatalErrorText2") + " " + Environment.NewLine + Environment.NewLine + errorLogPath + Environment.NewLine + Environment.NewLine : "") + (crashLogCopied ? "If the issue is repeatable, contact the {1} staff at {2} and provide the crash log file.".L10N("Client:Main:FatalErrorText3") : "If the issue is repeatable, contact the {1} staff at {2}.".L10N("Client:Main:FatalErrorText4")), MainClientConstants.GAME_NAME_LONG, MainClientConstants.GAME_NAME_SHORT, MainClientConstants.SUPPORT_URL_SHORT); MainClientConstants.DisplayErrorAction("KABOOOOOOOM".L10N("Client:Main:FatalErrorTitle"), error, true); } [SupportedOSPlatform("windows")] private static void CheckPermissions() { if (UserHasDirectoryAccessRights(ProgramConstants.GamePath, FileSystemRights.Modify)) return; string error = string.Format(("You seem to be running {0} from a write-protected directory.\n\n" + "For {1} to function properly when run from a write-protected directory, it needs administrative privileges.\n\n" + "Please also make sure that your security software isn't blocking {1}.").L10N("Client:Main:AdminRequiredExplanation"), MainClientConstants.GAME_NAME_LONG, MainClientConstants.GAME_NAME_SHORT); string question = "Would you like to restart the client with administrative rights?".L10N("Client:Main:AdminRequiredRestartPrompt"); string title = "Administrative privileges required".L10N("Client:Main:AdminRequiredTitle"); #if WINFORMS && NETFRAMEWORK DialogResult result = MessageBox.Show(error + "\n\n" + question, title, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2); if (result == DialogResult.Yes) { AdminRestarter.RestartAsAdmin(); } #else MainClientConstants.DisplayErrorAction(title, error, true); #endif Environment.Exit(1); } /// /// Checks whether the client has specific file system rights to a directory. /// See ssds's answer at https://stackoverflow.com/questions/1410127/c-sharp-test-if-user-has-write-access-to-a-folder /// /// The path to the directory. /// The file system rights. [SupportedOSPlatform("windows")] private static bool UserHasDirectoryAccessRights(string path, FileSystemRights accessRights) { var currentUser = WindowsIdentity.GetCurrent(); var principal = new WindowsPrincipal(currentUser); // If the user is not running the client with administrator privileges in Program Files, they need to be prompted to do so. if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) { string progfiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string progfilesx86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); if (ProgramConstants.GamePath.Contains(progfiles) || ProgramConstants.GamePath.Contains(progfilesx86)) return false; } var isInRoleWithAccess = false; try { var di = new DirectoryInfo(path); var acl = di.GetAccessControl(); var rules = acl.GetAccessRules(true, true, typeof(NTAccount)); foreach (AuthorizationRule rule in rules) { var fsAccessRule = rule as FileSystemAccessRule; if (fsAccessRule == null) continue; if ((fsAccessRule.FileSystemRights & accessRights) > 0) { var ntAccount = rule.IdentityReference as NTAccount; if (ntAccount == null) continue; try { if (principal.IsInRole(ntAccount.Value)) { if (fsAccessRule.AccessControlType == AccessControlType.Deny) return false; isInRoleWithAccess = true; } } catch (SecurityException) { //IsInRole may throw for selected roles when running in Wine, keep iterating other rules continue; } } } } catch (UnauthorizedAccessException) { return false; } return isInRoleWithAccess; } } } ================================================ FILE: DXMainClient/Program.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; #if !NETFRAMEWORK using System.Runtime.Loader; #endif using System.Threading; /* !! We cannot use references to other projects or non-framework assemblies in this class, assembly loading events not hooked up yet !! */ namespace DTAClient { static class Program { static Program() { /* We have different binaries depending on build platform, but for simplicity * the target projects (DTA, TI, MO, YR) supply them all in a single download. * To avoid DLL hell, we load the binaries from different directories * depending on the build platform. */ DirectoryInfo currentDir = new FileInfo(Assembly.GetEntryAssembly().Location).Directory; string startupPath = SearchResourcesDir(currentDir.FullName); string binariesFolderName = "Binaries"; #if !NETFRAMEWORK binariesFolderName = "BinariesNET8"; #endif COMMON_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName) + Path.DirectorySeparatorChar; #if XNA SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, "XNA") + Path.DirectorySeparatorChar; #elif GL && ISWINDOWS SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, "OpenGL") + Path.DirectorySeparatorChar; #elif GL && !ISWINDOWS SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, "UniversalGL") + Path.DirectorySeparatorChar; #elif DX SPECIFIC_LIBRARY_PATH = Path.Combine(startupPath, binariesFolderName, "Windows") + Path.DirectorySeparatorChar; #else #error Yuri has won #endif #if !DEBUG #if !NETFRAMEWORK // Set up DLL load paths as early as possible AssemblyLoadContext.Default.Resolving += DefaultAssemblyLoadContextOnResolving; #else // Set up DLL load paths as early as possible AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; #endif #endif } private static string COMMON_LIBRARY_PATH; private static string SPECIFIC_LIBRARY_PATH; static void InitializeApplicationConfiguration() { #if WINFORMS #if NET6_0_OR_GREATER // .NET 6.0 brings a source generator ApplicationConfiguration which is not available in previous .NET versions // https://medium.com/c-sharp-progarmming/whats-new-in-windows-forms-in-net-6-0-840c71856751 ApplicationConfiguration.Initialize(); #else #if NETCOREAPP3_0_OR_GREATER #if GL System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.SystemAware); #else System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.PerMonitorV2); #endif #endif System.Windows.Forms.Application.EnableVisualStyles(); System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); #endif #else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) SetProcessDPIAware(); #endif } [DllImport("user32.dll")] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [SupportedOSPlatform("windows")] private static extern bool SetProcessDPIAware(); /// /// The main entry point for the application. /// #if WINFORMS [STAThread] #endif static void Main(string[] args) { // https://stackoverflow.com/questions/3967716/how-to-find-encoding-for-1251-codepage Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); InitializeApplicationConfiguration(); bool noAudio = false; bool multipleInstanceMode = false; List unknownStartupParams = new List(); for (int arg = 0; arg < args.Length; arg++) { string argument = args[arg].ToUpperInvariant(); switch (argument) { case "-NOAUDIO": noAudio = true; break; case "-MULTIPLEINSTANCE": multipleInstanceMode = true; break; default: unknownStartupParams.Add(argument); break; } } var parameters = new StartupParams(noAudio, multipleInstanceMode, unknownStartupParams); if (multipleInstanceMode) { // Proceed to client startup PreStartup.Initialize(parameters); return; } // We're a single instance application! // http://stackoverflow.com/questions/229565/what-is-a-good-pattern-for-using-a-global-mutex-in-c/229567 // Global prefix means that the mutex is global to the machine string mutexId = FormattableString.Invariant($"Global{Guid.Parse("1CC9F8E7-9F69-4BBC-B045-E734204027A9")}"); using var mutex = new Mutex(false, mutexId, out _); bool hasHandle = false; try { try { hasHandle = mutex.WaitOne(8000, false); if (hasHandle == false) throw new TimeoutException("Timeout waiting for exclusive access"); } catch (AbandonedMutexException) { hasHandle = true; } catch (TimeoutException) { return; } // Proceed to client startup PreStartup.Initialize(parameters); } finally { if (hasHandle) mutex.ReleaseMutex(); } } #if !NETFRAMEWORK private static Assembly DefaultAssemblyLoadContextOnResolving(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName) { if (assemblyName.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) return null; // the specific dll should be in priority than the common one var specificFileInfo = new FileInfo(Path.Combine(SPECIFIC_LIBRARY_PATH, FormattableString.Invariant($"{assemblyName.Name}.dll"))); if (specificFileInfo.Exists) return assemblyLoadContext.LoadFromAssemblyPath(specificFileInfo.FullName); var commonFileInfo = new FileInfo(Path.Combine(COMMON_LIBRARY_PATH, FormattableString.Invariant($"{assemblyName.Name}.dll"))); if (commonFileInfo.Exists) return assemblyLoadContext.LoadFromAssemblyPath(commonFileInfo.FullName); return null; } #else private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { string unresolvedAssemblyName = args.Name.Split(',').First(); if (unresolvedAssemblyName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) return null; // the specific dll should be in priority than the common one var specificFileInfo = new FileInfo(FormattableString.Invariant($"{Path.Combine(SPECIFIC_LIBRARY_PATH, unresolvedAssemblyName)}.dll")); if (specificFileInfo.Exists) return Assembly.Load(AssemblyName.GetAssemblyName(specificFileInfo.FullName)); var commonFileInfo = new FileInfo(FormattableString.Invariant($"{Path.Combine(COMMON_LIBRARY_PATH, unresolvedAssemblyName)}.dll")); if (commonFileInfo.Exists) return Assembly.Load(AssemblyName.GetAssemblyName(commonFileInfo.FullName)); return null; } #endif /// /// 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."); } } } ================================================ FILE: DXMainClient/Properties/launchSettings.json ================================================ { "profiles": { "DXMainClient": { "commandName": "Project" }, "WSL": { "commandName": "WSL2", "distributionName": "" } } } ================================================ FILE: DXMainClient/Resources/ClientDefinitions.ini ================================================ ; Dawn of the Tiberium Age CnCNet Client Definitions [Themes] 0=Default theme,Default Theme\ [Settings] ; Set type of the game used with client. Allowed options: TS, YR, Ares ClientGameType=TS ; The executable that is started by the client after creating the .dxfail ; file if DirectX11 initialization fails LauncherExe=TiberianSun.exe ; The filename of the game executable to launch ; Accepts multiple entries separated with a comma, ; but currently only the first entry is ever used GameExecutableNames=game.exe ; Game executable name for Linux/Mac systems UnixGameExecutableName=wine-ts.sh ; List of executables to check for DirectDraw compatibility mode issues ; Comma-separated list of executables in the execution chain ;CompatibilityCheckExecutables=CnCNetYRLauncher.exe,gamemd.exe,gamemd-spawn.exe ; The main map file extension that is read by the client (e.g. map for TS/RA2, ini for Dune) MapFileExtension=map ExtraCommandLineParams=-CD. ; The filename of the client and game settings configuration file SettingsFile=SUN.ini ; Path to the file that defines multiplayer maps MPMapsPath=INI\MPMaps.ini ; Game modes that custom maps are allowed to show up in AllowedCustomGameModes=Custom Map,Standard ; The number of loading screens for each side in the game LoadingScreenCount=2 ; The local game, corresponding to game definitions in ClientCore LocalGame=TS ; Long game name, displayed in various places in the UI LongGameName=Tiberian Sun ; The name that's displayed as the window's title WindowTitle=Tiberian Sun Client ; Install path in registry, from HKEY_CURRENT_USER\Software\ RegistryInstallPath=TiberianSun ; CnCNet 5 live status identifier CnCNetLiveStatusIdentifier=cncnet5_ts ; The URL to open when the user clicks on "Credits" in the Extras menu CreditsURL=https://downloads.cncnet.org/updates/games/ts/live/credits.htm ; The URL to open when the user clicks on the change log link in the update query window ChangelogURL=https://downloads.cncnet.org/updates/games/ts/live/changelog.txt ; The support URL displayed when the client crashes or fails to update LongSupportURL=http://ppmforums.com/index.php?f=24 ; The shortened version of the above-mentioned support URL ShortSupportURL=ppmforums.com/index.php?f=24 ; File to launch when the user wants to open the map editor MapEditorExePath=Map Editor\Map Editor.bat ; Path to FinalSun map editor configuration file, the client writes its install path to the file FSIniPath=Map Editor\FinalSun.ini ; File name of BattleFS.ini equivalent, in the INI\ directory ; It's recommended to just write all campaigns in Battle.ini instead BattleFSFileName=BattleFS.ini ; Whether to write SidebarHack=true to spawn.ini on game launch ; Enable or disable this if GDI and Nod are unintentionally using each other's sidebars. SidebarHack=yes ; The minimum width that the client window is rendered at, ; smaller resolutions are downscaled MinimumRenderWidth=1024 ; The minimum height that the client window is rendered at, ; smaller resolutions are downscaled MinimumRenderHeight=600 ; The maximum width that the client window is rendered at, ; larger resolutions are upscaled MaximumRenderWidth=1280 ; The maximum height that the client window is rendered at, ; larger resolutions are upscaled MaximumRenderHeight=720 ; Resolutions that are given the "(Recommended)" suffix in the options window RecommendedResolutions=1280x720 ;,1360x768,1366x768,2560x1440 ; Hotkey configuration file for the hotkey configuration window ; in the options menu KeyboardINI=Keyboard.ini ; Hotkey section in KeyboardINI, for RA this should be WinHotkeys KeyboardHotkeySection=Hotkey ; The name of the log file to parse statistics from at the end of the game StatisticsLogFileName=TS.LOG ; Whether or not previously used game options in skirmish are saved across client sessions SaveSkirmishGameOptions=yes ; Whether or not previously used game options in campaign are saved across client sessions SaveCampaignGameOptions=yes ; Discord application ID for Discord integration DiscordAppId=1041056809349304441 ; The filename of the QuickMatch executable to launch Quickmatch=CnCNetQM.exe ; Set to true to disable the updater and to hide the "cheater!" dialog when modding the game ModMode=true ; Activates warnings for non-release build of XNA Client. ; Please, make sure you are not publishing stable mod version with unstable development client build. ShowDevelopmentBuildWarnings=true ================================================ FILE: DXMainClient/Resources/DTA/CampaignSelector.ini ================================================ [INISystem] BasedOn=GenericWindow.ini [CampaignSelector] BackgroundTexture=MainMenu/dbak.png DrawMode=Centered DrawBorders=false Size=672,600 [lbCampaignList] Size=322,554 EnableScrollbar=true [lblMissionDescriptionHeader] TextAnchor=RIGHT AnchorPoint=346,12 [tbMissionDescription] Location=346,34 DistanceFromRightBorder=12 [lblDifficultyLevel] TextAnchor=HORIZONTAL_CENTER AnchorPoint=503,476 [trbDifficultySelector] Location=346,498 DistanceFromRightBorder=12 [lblEasy] TextAnchor=RIGHT AnchorPoint=346,534 [lblNormal] TextAnchor=HORIZONTAL_CENTER AnchorPoint=503,534 [lblHard] TextAnchor=LEFT AnchorPoint=660,534 [lblDifficultyNames] Location=346,538 [btnLaunch] IdleTexture=147pxbtn.png HoverTexture=147pxbtn_c.png Width=147 DistanceFromRightBorder=179 DistanceFromBottomBorder=12 [btnCancel] IdleTexture=147pxbtn.png HoverTexture=147pxbtn_c.png Width=147 DistanceFromRightBorder=12 ================================================ FILE: DXMainClient/Resources/DTA/CheaterScreen.ini ================================================ [INISystem] BasedOn=GenericWindow.ini [lblCheater] Text=Modifications Detected! Location=97,12 [lblDescription] Text=Modified game files have been detected. They could@affect the game experience.@@Are you sure you want to have an inauthentic experience@playing this mission/campaign? [imagePanel] BackgroundTexture=cheater.png Location=27,124 Size=280,280 ;310,x ================================================ FILE: DXMainClient/Resources/DTA/CnCNetGameLobby.ini ================================================ [INISystem] BasedOn=MultiplayerGameLobby.ini [MultiplayerGameLobby] $CCMP99=btnChangeTunnel:XNAClientButton $CCMP100=btnGameLobbySettings:XNAClientButton [btnChangeTunnel] $Width=133 $X=getX(btnLeaveGame) - getWidth($Self) - BUTTON_SPACING $Y=getY(btnLaunchGame) Text=Change Tunnel [btnGameLobbySettings] $Width=133 $X=getX(btnChangeTunnel) - getWidth($Self) - BUTTON_SPACING $Y=getY(btnLaunchGame) Text=Lobby Settings ================================================ FILE: DXMainClient/Resources/DTA/CnCNetLobby.ini ================================================ [INISystem] BasedOn=LANLobby.ini ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Configs/aqrit.cfg ================================================ ;;; www.bitpatch.com ;;; RealDDrawPath = AUTO BltMirror = 0 BltNoTearing = 0 ColorFix = 0 DisableHighDpiScaling = 0 FakeVsync = 0 FakeVsyncInterval = 0 ForceBltNoTearing = 0 ForceDirectDrawEmulation = 1 NoVideoMemory = 0 SingleProcAffinity = 0 ShowFPS = 0 ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Configs/cnc-ddraw.ini ================================================ ; cnc-ddraw - https://github.com/CnCNet/cnc-ddraw - https://cncnet.org [ddraw] ; ### Optional settings ### ; Use the following settings to adjust the look and feel to your liking ; Stretch to custom resolution, 0 = defaults to the size game requests width=0 height=0 ; Override the width/height settings shown above and always stretch to fullscreen ; Note: Can be combined with 'windowed=true' to get windowed-fullscreen aka borderless mode fullscreen=false ; Run in windowed mode rather than going fullscreen windowed=false ; Maintain aspect ratio - (Requires 'handlemouse=true') maintas=false ; Windowboxing / Integer Scaling - (Requires 'handlemouse=true') boxing=false ; Real rendering rate, -1 = screen rate, 0 = unlimited, n = cap ; Note: Does not have an impact on the game speed, to limit your game speed use 'maxgameticks=' maxfps=125 ; Vertical synchronization, enable if you get tearing - (Requires 'renderer=auto/opengl/direct3d9') ; Note: vsync=true can fix tearing but it will cause input lag vsync=false ; Automatic mouse sensitivity scaling - (Requires 'handlemouse=true') ; Note: Only works if stretching is enabled. Sensitivity will be adjusted according to the size of the window adjmouse=false ; Preliminary libretro shader support - (Requires 'renderer=opengl') https://github.com/libretro/glsl-shaders ; 2x scaling example: https://imgur.com/a/kxsM1oY - 4x scaling example: https://imgur.com/a/wjrhpFV shader= ;Shaders\interpolation\bilinear.glsl ; Window position, -32000 = center to screen posX=-32000 posY=-32000 ; Renderer, possible values: auto, opengl, gdi, direct3d9 (auto = try direct3d9/opengl, fallback = gdi) renderer=auto ; Developer mode (don't lock the cursor) devmode=false ; Show window borders in windowed mode border=true ; Save window position/size/state on game exit and restore it automatically on next game start ; Possible values: 0 = disabled, 1 = save to global 'ddraw' section, 2 = save to game specific section savesettings=1 ; Should the window be resizeable by the user in windowed mode? resizeable=true ; Enable C&C video resize hack - Stretches C&C cutscenes to fullscreen vhack=false ; Enable linear (D3DTEXF_LINEAR) upscaling filter for the direct3d9 renderer (16 bit color depth games only) d3d9linear=true ; ### Compatibility settings ### ; Use the following settings in case there are any issues with the game ; Hide WM_ACTIVATEAPP and WM_NCACTIVATE messages to prevent problems on alt+tab noactivateapp=false ; Max game ticks per second, possible values: -1 = disabled, 0 = emulate 60hz vblank, 1-1000 = custom game speed ; Note: Can be used to slow down a too fast running game, fix flickering or too fast animations ; Note: Usually one of the following values will work: 60 / 30 / 25 / 20 / 15 (lower value = slower game speed) maxgameticks=0 ; Gives cnc-ddraw full control over the mouse cursor (required for adjmouse/boxing/maintas) ; Note: Set this to 'false' if your cursor becomes invisible at some places in the game handlemouse=true ; Windows API Hooking, Possible values: 0 = disabled, 1 = IAT Hooking, 2 = Microsoft Detours, 3 = IAT+Detours Hooking (All Modules), 4 = IAT Hooking (All Modules) ; Note: Change this value if windowed mode or upscaling isn't working properly ; Note: 'hook=2' will usually work for problematic games, but 'hook=2' must be combined with renderer=gdi hook=4 ; Force minimum FPS, possible values: 0 = disabled, -1 = use 'maxfps=' value, 1-1000 = custom FPS ; Note: Set this to a low value such as 5 or 10 if some parts of the game are not being displayed (e.g. menus or loading screens) minfps=0 ; Disable fullscreen-exclusive mode for the direct3d9/opengl renderers ; Note: Can be used in case some GUI elements like buttons/textboxes/videos/etc.. are invisible nonexclusive=false ; Force CPU0 affinity, avoids crashes/freezing, *might* have a performance impact singlecpu=true ; ### Game specific settings ### ; The following settings override all settings shown above, section name = executable name ; Command & Conquer: Red Alert - CnCNet [ra95-spawn] maxfps=125 ; Command & Conquer Gold - CnCNet [cnc95] maxfps=125 ; Carmageddon [CARMA95] renderer=opengl noactivateapp=true maxgameticks=60 ; Carmageddon [CARM95] renderer=opengl noactivateapp=true maxgameticks=60 ; Command & Conquer Gold [C&C95] maxgameticks=120 maxfps=60 minfps=-1 ; Command & Conquer: Red Alert [ra95] maxgameticks=120 maxfps=60 minfps=-1 ; Command & Conquer: Red Alert [ra95p] maxfps=60 minfps=-1 ; Age of Empires [empires] handlemouse=false ; Age of Empires: The Rise of Rome [empiresx] handlemouse=false ; Age of Empires II [EMPIRES2] handlemouse=false ; Age of Empires II: The Conquerors [age2_x1] handlemouse=false ; Outlaws [olwin] noactivateapp=true maxgameticks=60 handlemouse=false renderer=gdi ; Dark Reign: The Future of War [DKReign] maxgameticks=60 ; Star Wars: Galactic Battlegrounds [battlegrounds] handlemouse=false ; Star Wars: Galactic Battlegrounds: Clone Campaigns [battlegrounds_x1] handlemouse=false ; Carmageddon 2 [Carma2_SW] renderer=opengl noactivateapp=true maxgameticks=60 ; Atomic Bomberman [BM] maxgameticks=60 ; Dune 2000 [dune2000] maxfps=59 accuratetimers=true ; Dune 2000 - CnCNet [dune2000-spawn] maxfps=59 accuratetimers=true ; Command & Conquer: Tiberian Sun / Command & Conquer: Red Alert 2 [game] checkfile=.\blowfish.dll noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Tiberian Sun Demo [SUN] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Tiberian Sun - CnCNet [ts-spawn] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Red Alert 2 - XWIS [ra2] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Red Alert 2 - XWIS [Red Alert 2] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Red Alert 2: Yuri's Revenge [gamemd] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Red Alert 2: Yuri's Revenge - ?ModExe? [ra2md] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Red Alert 2: Yuri's Revenge - CnCNet [gamemd-spawn] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Command & Conquer: Red Alert 2: Yuri's Revenge - XWIS [Yuri's Revenge] noactivateapp=true handlemouse=false maxfps=60 minfps=-1 ; Twisted Metal [TWISTED] renderer=opengl nonexclusive=true maxgameticks=25 minfps=5 ; Twisted Metal 2 [Tm2] renderer=opengl nonexclusive=true maxgameticks=60 handlemouse=false fixchildwindows=false ; Caesar III [c3] handlemouse=false sierrahack=true ; Pharaoh [Pharaoh] handlemouse=false sierrahack=true ; Master of Olympus - Zeus [Zeus] handlemouse=false sierrahack=true renderer=gdi hook=2 ; Dungeon Keeper 2 [DKII] maxgameticks=60 noactivateapp=true dk2hack=true ; Chris Sawyer's Locomotion [LOCO] handlemouse=false ; Age of Wonders [AoWSM] windowed=true fullscreen=false renderer=gdi hook=2 ; Age of Wonders 2 [AoW2] windowed=true fullscreen=false renderer=gdi hook=2 ; Stronghold Crusader HD [Stronghold Crusader] handlemouse=false ; Stronghold Crusader Extreme HD [Stronghold_Crusader_Extreme] handlemouse=false ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Configs/ddraw-auto.ini ================================================ ; ts-ddraw - https://github.com/CnCNet/ts-ddraw ; use the following settings to enable the experimental stretching support ; works only fullscreen right now + menus are not centered [ddraw] ; stretch to custom resolution, 0 = defaults to the size game requests ;StretchToWidth=2560 ;StretchToHeight=1440 ; override StretchToWidth/StretchToHeight and always stretch to fullscreen ;StretchToFullscreen=No ; use windowboxing to make a best fit ;Windowboxing=No ; maintain aspect ratio - this setting is ignored when Windowboxing is enabled ;MaintainAspectRatio=No ; Enable vertical sync ;VSync=No ; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI Renderer=auto ; Draw the FPS overlay 0 = no, 1 = yes ;DrawFPS=0 ; Use 2 textures, in rotation, for the Primary Surface, default = yes ;PrimarySurface2Tex=Yes ; Use single processor affinity to prevent thread crashes ;SingleProcAffinity=Yes ; Set the Renderer FPS value, Default = 60, unless VSync ;TargetFPS=60 ; Fixed output display setting. Accepted values are stretch, center & default. Default = stretch. ; FixedOutput=stretch ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Configs/ddraw-gdi.ini ================================================ ; ts-ddraw - https://github.com/CnCNet/ts-ddraw ; use the following settings to enable the experimental stretching support ; works only fullscreen right now + menus are not centered [ddraw] ; stretch to custom resolution, 0 = defaults to the size game requests ;StretchToWidth=2560 ;StretchToHeight=1440 ; override StretchToWidth/StretchToHeight and always stretch to fullscreen ;StretchToFullscreen=No ; use windowboxing to make a best fit ;Windowboxing=No ; maintain aspect ratio - this setting is ignored when Windowboxing is enabled ;MaintainAspectRatio=No ; Enable vertical sync ;VSync=No ; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI Renderer=gdi ; Draw the FPS overlay 0 = no, 1 = yes ;DrawFPS=0 ; Use 2 textures, in rotation, for the Primary Surface, default = yes ;PrimarySurface2Tex=Yes ; Use single processor affinity to prevent thread crashes ;SingleProcAffinity=Yes ; Set the Renderer FPS value, Default = 60, unless VSync ;TargetFPS=60 ; Fixed output display setting. Accepted values are stretch, center & default. Default = stretch. ; FixedOutput=stretch ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Configs/ddraw-opengl.ini ================================================ ; ts-ddraw - https://github.com/CnCNet/ts-ddraw ; use the following settings to enable the experimental stretching support ; works only fullscreen right now + menus are not centered [ddraw] ; stretch to custom resolution, 0 = defaults to the size game requests ;StretchToWidth=2560 ;StretchToHeight=1440 ; override StretchToWidth/StretchToHeight and always stretch to fullscreen ;StretchToFullscreen=No ; use windowboxing to make a best fit ;Windowboxing=No ; maintain aspect ratio - this setting is ignored when Windowboxing is enabled ;MaintainAspectRatio=No ; Enable vertical sync ;VSync=No ; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI Renderer=opengl ; Draw the FPS overlay 0 = no, 1 = yes ;DrawFPS=0 ; Use 2 textures, in rotation, for the Primary Surface, default = yes ;PrimarySurface2Tex=Yes ; Use single processor affinity to prevent thread crashes ;SingleProcAffinity=Yes ; Set the Renderer FPS value, Default = 60, unless VSync ;TargetFPS=60 ; Fixed output display setting. Accepted values are stretch, center & default. Default = stretch. ; FixedOutput=stretch ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Configs/dxwnd.ini ================================================ [DxWnd] Enabled=Yes Emulation=1 DisableMaxWindowedMode=Yes RunInWindow=True ClipCursor=Yes ForceClipper=Yes NoWindowFrame=False ForceDirectDrawEmulation=No [keymapping] refresh=0x74 workarea=0x7B fullscreen=0x0D ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Unix/wine-game.bat ================================================ Syringe.exe "gamemd.exe" -SPAWN -CD -LOG ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Unix/wine-game.sh ================================================ #!/bin/sh wineconsole Resources/Compatibility/Unix/wine-game.bat & BACK_PID=$! wait $BACK_PID ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Unix/wine-mapedit.bat ================================================ cd "Map Editor" Syringe.exe "REAlert2.dat" ================================================ FILE: DXMainClient/Resources/DTA/Compatibility/Unix/wine-mapedit.sh ================================================ #!/bin/sh wineconsole Resources/Compatibility/Unix/wine-mapedit.bat & BACK_PID=$! wait $BACK_PID ================================================ FILE: DXMainClient/Resources/DTA/Default Theme/DTACnCNetClient.ini ================================================ ; Dawn of the Tiberium Age (DTA) CnCNet Client settings ; Created by Rampastring ; http://www.moddb.com/members/rampastring ; If you use or redistribute the client in any public projects, please include ; Rampastring and Dawn of the Tiberium Age in your project's credits. ; Use DTACnCNetClient.ini to configure theme-specific settings. [General] ; The color of most strings in the UI. UILabelColor=181,251,0 ; The foreground color of usable (buttons, drop-down boxes, chat boxes etc.) UI items. AltUIColor=111,251,0 ; The background color of usable UI items. AltUIBackgroundColor=0,15,8 ; The color into which the font of buttons will change into when you hover over them (in RGB). ButtonHoverColor=252,252,252 ; The color of admins' names on the channel player lists. AdminNameColor=255,0,0 ; The default color of chat messages. Used for messages which lack color information (rare), as well as for private messages. DefaultChatColor=0,255,0 DefaultPersonalChatColorIndex=1 ; The color of sent private messages. PrivateMessageColor=160,160,160 ; The color of received private messages. PrivateMessageOtherUserColor=64,196,64 ;defaults to 64,64,168 ListBoxFocusColor=40,68,0 HoverOnGameColor=20,34,18 PanelBorderColor=74,182,222 WindowBorderColor=111,251,0 ; Button fade animations (1.0 = disabled) AlphaRate=1.0 ; Check-box tick animations CheckBoxAlphaRate=0.2 ;R,G,B,A MapPreviewNameBackgroundColor=0,0,0,144 MapPreviewNameBorderColor=181,251,0,144 MainMenuTheme=Default Theme\MainMenuTheme StartingLocationsUsePlayerRemapColor=yes [GameLobby] ; Color of the Co-Op briefing text and outline in R,G,B CoopBriefingForeColor=0,255,0 ; Color to use when a non-default option is selected in a combo box. Defaults to red. ComboBoxNondefaultColor=255,255,255 [ParserConstants] EMPTY_SPACE_TOP=12 EMPTY_SPACE_BOTTOM=11 EMPTY_SPACE_SIDES=22 LOBBY_PANEL_SPACING=10 BUTTON_SPACING=12 CHECKBOX_SPACING=25 ================================================ FILE: DXMainClient/Resources/DTA/ExtrasWindow.ini ================================================ [ExtrasWindow] Size=1280,720 DrawBorders=false BackgroundTexture=MainMenu/mainmenuebg.png [ExtraControls] 0=Logo:XNAExtraPanel 1=btnLan:XNALinkButton [Logo] Location=369,8 BackgroundTexture=MainMenu/Logo.png [btnLan] Location=490,261 IdleTexture=MainMenu/lan.png HoverTexture=MainMenu/lan_c.png Text= HoverSoundEffect=MainMenu/button.wav [btnExMapEditor] Location=490,315 IdleTexture=MainMenu/mapeditor.png HoverTexture=MainMenu/mapeditor_c.png Text= HoverSoundEffect=MainMenu/button.wav [btnExStatistics] Location=490,369 IdleTexture=MainMenu/statistics.png HoverTexture=MainMenu/statistics_c.png Text= HoverSoundEffect=MainMenu/button.wav [btnExCredits] Location=490,423 IdleTexture=MainMenu/credits.png HoverTexture=MainMenu/credits_c.png Text= HoverSoundEffect=MainMenu/button.wav [btnExCancel] Location=490,477 IdleTexture=MainMenu/back.png HoverTexture=MainMenu/back_c.png Text= HoverSoundEffect=MainMenu/button.wav [lblCnCNetStatus] Text=TS Players on CnCNet RemapColor=45,228,255 Location=1099,67 DistanceFromRightBorder=149 [lblCnCNetPlayerCount] RemapColor=45,228,255 Location=990,67 DistanceFromRightBorder=286 [txtVersion] Text=Version: RemapColor=45,228,255 Location=990,91 DistanceFromRightBorder=252 [lblVersion] RemapColor=45,228,255 IdleColor=45,228,255 HoverColor=255,255,255 Location=1304,91 DistanceFromRightBorder=246 [lblUpdateStatus] RemapColor=45,228,255 IdleColor=45,228,255 HoverColor=255,255,255 Location=990,115 DistanceFromRightBorder=129 ================================================ FILE: DXMainClient/Resources/DTA/GameCollectionConfig.ini ================================================ [CustomGames] 0=PP 1=UNKN [PP] InternalName=pp UIName=Project Phantom ChatChannel=#projectphantom GameBroadcastChannel=#projectphantom-games ClientExecutableName=PPLauncher.exe RegistryInstallPath=HKLM\Software\ProjectPhantom IconFilename=ppicon.png [UNKN] InternalName=unkn UIName=Unknown (Example) ChatChannel=#unkn GameBroadcastChannel=#unkn-games ClientExecutableName=unknown.exe RegistryInstallPath=HKLM\Software\nonexistent-99aed32b-4428-4724-8f49-e8aa0e87c675 IconFilename=nonexistent-99aed32b-4428-4724-8f49-e8aa0e87c675.png ================================================ FILE: DXMainClient/Resources/DTA/GameLobbyBase.ini ================================================ [INISystem] BasedOn=GenericWindow.ini [SkirmishLobby] BackgroundTexture=MainMenu/dbak.png DrawMode=Centered ;SolidColorBackgroundTexture=0,24,72,128 PlayerOptionLocationX=11 ;22 ;def=25 PlayerOptionLocationY=25 ;def=24 PlayerOptionVerticalMargin=9 ;def=12 PlayerOptionHorizontalMargin=5 ;def=3 PlayerOptionCaptionLocationY=6 ;def=6 PlayerNameWidth=128 ;117; def=136 SideWidth=86 ;def=91 ColorWidth=70 ;def=79 StartWidth=0 ;def=49 TeamWidth=44 ;def=46 $CC00=btnLaunchGame:GameLaunchButton $CC01=btnLeaveGame:XNAClientButton $CC03=MapPreviewBox:MapPreviewBox $CC04=GameOptionsPanel:XNAPanel $CC05=PlayerOptionsPanel:XNAPanel $CC06=lblMapName:XNALabel $CC07=lblMapAuthor:XNALabel $CC08=lblGameMode:XNALabel $CC09=lblMapSize:XNALabel $CC12=lbMapList:XNAMultiColumnListBox $CC13=lblGameModeSelect:XNALabel $CC14=ddGameMode:XNAClientDropDown $CC15=tbMapSearch:XNASuggestionTextBox $CC16=btnPickRandomMap:XNAClientButton $CC17=btnSaveLoadGameOptions:XNAClientButton [btnLaunchGame] Text=Launch Game ;TextShadowDistance=2 $Width=133 $X=EMPTY_SPACE_SIDES $Y=getHeight($ParentControl) - getHeight($Self) - EMPTY_SPACE_BOTTOM [btnLeaveGame] $Width=133 ;TextShadowDistance=2 $X=getWidth($ParentControl) - getWidth($Self) - EMPTY_SPACE_SIDES $Y=getY(btnLaunchGame) Text=Main Menu [MapPreviewBox] SolidColorBackgroundTexture=0,0,0,192 $Width=802 $X=getWidth($ParentControl) - getWidth($Self) - EMPTY_SPACE_SIDES $Y=316 $Height=getHeight($ParentControl) - getY($Self) - 46 [lblMapName] Text=Map: FontIndex=1 $TextAnchor=CENTER $AnchorPoint=getX(MapPreviewBox) + (getWidth(MapPreviewBox) / 2),getY(MapPreviewBox) - getHeight($Self) [lblMapAuthor] FontIndex=1 $TextAnchor=LEFT $AnchorPoint=getRight(MapPreviewBox),getY(lblMapName) [lblGameMode] FontIndex=1 $TextAnchor=RIGHT $AnchorPoint=getX(MapPreviewBox),getY(lblMapName) [lblMapSize] FontIndex=0 ;3 $TextAnchor=LEFT $AnchorPoint=getRight(MapPreviewBox) - 3,getBottom(MapPreviewBox) - 16 [lbMapList] SolidColorBackgroundTexture=0,0,0,192 $X=EMPTY_SPACE_SIDES $Y=EMPTY_SPACE_TOP + 28 $Width=getWidth($ParentControl) - (getX($Self) + (getWidth(MapPreviewBox) + EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING) $Height=getBottom(MapPreviewBox) - getY($Self) [lblGameModeSelect] Text=GAME MODE: FontIndex=1 $TextAnchor=RIGHT $AnchorPoint=getX(lbMapList),getY(lbMapList) - getHeight($Self) - 8 [ddGameMode] $Width=150 $Height=21 $X=getRight(lbMapList) - getWidth($Self) $Y=getY(lbMapList) - getHeight($Self) - 7 [tbMapSearch] Suggestion=Search map... $Width=getWidth(lbMapList) - 15 $Height=19 $X=getX(lbMapList) + 15 $Y=getY(lbMapList) [btnPickRandomMap] Text=Random ;TextShadowDistance=2 $Width=75 $Height=17 $X=getRight(lbMapList) - getWidth($Self) - 1 $Y=getY(lbMapList) + 1 [PlayerOptionsPanel] SolidColorBackgroundTexture=0,0,0,192 DrawBorders=yes $X=getX(MapPreviewBox) $Y=EMPTY_SPACE_TOP $Width=getWidth($ParentControl) - (getX($Self) + (getWidth(GameOptionsPanel) + EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING) ;365 $Height=getHeight(GameOptionsPanel) [btnSaveLoadGameOptions] IdleTexture=comboBoxArrow.png HoverTexture=comboBoxArrow.png $Width=18 $Height=21 $X=getRight(GameOptionsPanel) - getWidth($Self) - 1 $Y=getY(GameOptionsPanel) + 1 [GameOptionsPanel] SolidColorBackgroundTexture=0,0,0,192 DrawBorders=yes $Width=427 $Height=266 $X=getWidth($ParentControl) - getWidth($Self) - EMPTY_SPACE_SIDES $Y=EMPTY_SPACE_TOP $CC_00=cmbTSFS:GameLobbyDropDown $CC_01=lblTSFS:XNALabel $CC_02=cmbTechLevel:GameLobbyDropDown $CC_03=lblTechLevel:XNALabel $CC_04=cmbCredits:GameLobbyDropDown $CC_05=lblCredits:XNALabel $CC_06=cmbUnitCount:GameLobbyDropDown $CC_07=lblUnitCount:XNALabel $CC_08=cmbGameSpeedCap:GameLobbyDropDown $CC_09=lblGameSpeedCap:XNALabel $CC_10=chkBases:GameLobbyCheckBox $CC_11=chkShortGame:GameLobbyCheckBox $CC_12=chkRedeplMCV:GameLobbyCheckBox $CC_13=chkMultiEng:GameLobbyCheckBox $CC_14=chkDestrBridges:GameLobbyCheckBox $CC_15=chkCrates:GameLobbyCheckBox $CC_16=chkVisceroids:GameLobbyCheckBox $CC_17=chkNoBaddyCrates:GameLobbyCheckBox $CC_18=chkAttackNeutralUnits:GameLobbyCheckBox $CC_19=chkIngameAllying:XNACheckBox $CC_20=chkBuildOffAlly:GameLobbyCheckBox $CC_21=chkHarderAI:GameLobbyCheckBox $CC_22=chkVetBal:GameLobbyCheckBox $CC_23=chkInfiniteTiberium:GameLobbyCheckBox $CC_24=chkImmuneHarvs:GameLobbyCheckBox $CC_25=chkSilos:GameLobbyCheckBox $CC_26=chkAimableSams:GameLobbyCheckBox $CC_27=chkMultipleFactory:GameLobbyCheckBox $CC_28=chkSuperWeapons:GameLobbyCheckBox $CC_29=chkRevealShroud:GameLobbyCheckBox [cmbTSFS] $Width=97 $Height=21 $X=16 $Y=26 Items=Tiberian Sun,Firestorm DefaultIndex=0 SpawnIniOption=Firestorm DataWriteMode=Boolean [lblTSFS] Text=Game Type: $X=getX(cmbTSFS) $Y=getY(cmbTSFS) - 19 [cmbTechLevel] $Width=97 $Height=21 $X=getX(cmbTSFS) $Y=getY(cmbTSFS) + 52 OptionName=Tech Level Items=10,9,8,7,6,5,4,3,2,1 DefaultIndex=0 SpawnIniOption=TechLevel DataWriteMode=String [lblTechLevel] Text=Tech Level: $X=getX(cmbTechLevel) $Y=getY(cmbTechLevel) - 19 [cmbCredits] $Width=97 $Height=21 $X=getX(cmbTSFS) $Y=getY(cmbTechLevel) + 52 OptionName=Starting Credits Items=20000,15000,12500,10000,7500,5000,2500 DefaultIndex=3 SpawnIniOption=Credits DataWriteMode=String [lblCredits] Text=Starting Credits: $X=getX(cmbCredits) $Y=getY(cmbCredits) - 19 [cmbUnitCount] $Width=97 $Height=21 $X=getX(cmbTSFS) $Y=getY(cmbCredits) + 52 OptionName=Unit Count Items=10,9,8,7,6,5,4,3,2,1 DefaultIndex=9 SpawnIniOption=UnitCount DataWriteMode=String [lblUnitCount] Text=Unit Count: $X=getX(cmbUnitCount) $Y=getY(cmbUnitCount) - 19 [cmbGameSpeedCap] $Width=97 $Height=21 $X=getX(cmbTSFS) $Y=getY(cmbUnitCount) + 52 OptionName=Game Speed Items=Maximum,60 FPS,45 FPS,30 FPS,20 FPS,15 FPS,10 FPS DefaultIndex=1 SpawnIniOption=GameSpeed DataWriteMode=Index [lblGameSpeedCap] Text=Game Speed: $X=getX(cmbGameSpeedCap) $Y=getY(cmbGameSpeedCap) - 19 [chkBases] Text=Bases SpawnIniOption=Bases Checked=True ToolTip=Players start with Mobile Construction Vehicles. $X=133 $Y=11 [chkShortGame] Text=Short Game SpawnIniOption=ShortGame Checked=True ToolTip=Having only units and no structures left will cause the units to self-destruct and make the player instantly lose the game. $X=getX(chkBases) $Y=getY(chkBases) + CHECKBOX_SPACING [chkRedeplMCV] Text=Re-Deployable MCV SpawnIniOption=MCVRedeploy Checked=True ToolTip=Construction Yards can repack into a Mobile Construction Vehicle. $X=getX(chkShortGame) $Y=getY(chkShortGame) + CHECKBOX_SPACING [chkMultiEng] Text=Multi Engineer SpawnIniOption=MultiEngineer Checked=False ToolTip=Capturing a structure requires three Engineers instead of one. $X=getX(chkShortGame) $Y=getY(chkRedeplMCV) + CHECKBOX_SPACING [chkDestrBridges] Text=Destroyable Bridges SpawnIniOption=BridgeDestroy Checked=True ToolTip=You can destroy low bridges by force-firing on them. $X=getX(chkShortGame) $Y=getY(chkMultiEng) + CHECKBOX_SPACING [chkCrates] Text=Crates SpawnIniOption=Crates Checked=True ToolTip=Collectable crates will appear in random locations on the map, granting credits, tiberium, units, unit powerups, multi-missiles, area heal, global heal or booby traps. $X=getX(chkShortGame) $Y=getY(chkDestrBridges) + CHECKBOX_SPACING [chkNoBaddyCrates] Text=Safe Crates Only CustomIniPath=INI/Game Options/No Baddy Crates.ini Visible=False Checked=False ToolTip=No crates with potential negative effects will appear if crates are enabled. $X=getX(chkShortGame) $Y=getY(chkCrates) + CHECKBOX_SPACING [chkVisceroids] Text=Visceroids Reversed=yes ;make the checkbox set the associated key to =False instead of =True when enabled CustomIniPath=INI/Game Options/Disable Visceroids.ini Checked=True ToolTip=Infantry that die from walking over tiberium will become visceroids and some maps will already have visceroids present from the start. $X=getX(chkShortGame) $Y=getY(chkCrates) + CHECKBOX_SPACING [chkAttackNeutralUnits] Text=Auto-target Neutrals SpawnIniOption=AttackNeutralUnits Checked=True ToolTip=Units automatically attack armed neutral units. $X=getX(chkShortGame) $Y=getY(chkVisceroids) + CHECKBOX_SPACING [chkIngameAllying] Text=Ingame Allying ;Locked Teams SpawnIniOption=AlliesAllowed Checked=False Enabled=False AllowChecking=false ;Reversed=true ToolTip=Players can form and break alliances in the middle of the game by selecting a unit or structure of another human player and then pressing "A" on the keyboard. $X=getX(chkShortGame) $Y=getY(chkAttackNeutralUnits) + CHECKBOX_SPACING [chkBuildOffAlly] Text=Build Off Ally SpawnIniOption=BuildOffAlly Checked=False ToolTip=Allow building next to structures of teammates. $X=getX(chkShortGame) $Y=getY(chkIngameAllying) + CHECKBOX_SPACING [chkHarderAI] Text=Harder AI CustomIniPath=INI/Game Options/Harder AI.ini Checked=False ToolTip=The AI is much harder than the default AI of Tiberian Sun. $X=281 $Y=getY(chkBases) [chkVetBal] Text=Veteran Balance Patch CustomIniPath=INI/Game Options/Veteran Balance Patch.ini Checked=False ToolTip=The game will be rebalanced according to Veteran Balance Patch v2.50. $X=getX(chkHarderAI) $Y=getY(chkHarderAI) + CHECKBOX_SPACING [chkInfiniteTiberium] Text=Infinite Tiberium CustomIniPath=INI/Game Options/Infinite Tiberium.ini Checked=False ToolTip=Tiberium is much more valuable and lasts longer than normally. MapScoringMode=DenyWhenChecked $X=getX(chkHarderAI) $Y=getY(chkVetBal) + CHECKBOX_SPACING [chkImmuneHarvs] Text=Immune Harvesters CustomIniPath=INI/Game Options/Immune Harvesters.ini Checked=False ToolTip=Harvesters are indestructible (but are no longer able to crush infantry). ;Visible=False ;Enabled=False MapScoringMode=DenyWhenChecked $X=getX(chkHarderAI) $Y=getY(chkInfiniteTiberium) + CHECKBOX_SPACING [chkSilos] Text=Silos Needed CustomIniPath=INI/Game Options/No Silos.ini Checked=True Reversed=True ToolTip=You don't need to build extra Refineries/Silos to store a lot of credits. $X=getX(chkHarderAI) $Y=getY(chkImmuneHarvs) + CHECKBOX_SPACING [chkAimableSams] Text=Aimable SAMs SpawnIniOption=AimableSams Checked=False ToolTip=Allow players to give orders to target specific aircraft with the selected SAM Site(s). $X=getX(chkHarderAI) $Y=getY(chkSilos) + CHECKBOX_SPACING [chkMultipleFactory] Text=Multiple Factory Bonus SpawnIniOption=MultipleFactory EnabledSpawnIniValue=.85 DisabledSpawnIniValue=0 Checked=False ToolTip=Building multiple factories will speed up production of whatever that factory can produce by 17.6% per factory. $X=getX(chkHarderAI) $Y=getY(chkAimableSams) + CHECKBOX_SPACING [chkSuperWeapons] Text=Super Weapons Reversed=yes ;make the checkbox set the associated key to =False instead of =True when enabled CustomIniPath=INI/Game Options/Disable Super Weapons.ini Checked=True ToolTip=Players can use super weapons such as the multi-missile and ion cannon. $X=getX(chkHarderAI) $Y=getY(chkMultipleFactory) + CHECKBOX_SPACING [chkRevealShroud] Text=Revealed Map CustomIniPath=INI/Game Options/Reveal Shroud.ini Checked=False ToolTip=The map will be entirely unshrouded when the game starts. MapScoringMode=DenyWhenChecked $X=getX(chkHarderAI) $Y=getY(chkSuperWeapons) + CHECKBOX_SPACING ================================================ FILE: DXMainClient/Resources/DTA/GameOptions.ini ================================================ ; The Dawn of the Tiberium Age (DTA) CnCNet Client settings ; Created by Rampastring ; http://www.moddb.com/members/rampastring ; If you use or redistribute the client in any public projects, please include ; Rampastring and Dawn of the Tiberium Age in your project's credits. [General] Sides=GDI,Nod StartingLocationAngularVelocity=0.0075 ReservedStartingLocationAngularVelocity=0.05 RandomColor=168,168,168 ; The multiplayer colors. Syntax: =R,G,B, [MPColors] Gold=255,223,94,0 Red=222,0,0,1 Blue=39,60,179,2 Green=12,150,12,3 Orange=255,145,0,4 Cyan=20,177,255,5 Purple=185,20,255,6 Pink=255,94,199,7 ; Keys that are ALWAYS written to spawn.ini when the game starts. ; These can be overriden by gamemode-specific code. [ForcedSpawnIniOptions] ;Bases=Yes FogOfWar=No SidebarHack=Yes Protocol=0 ================================================ FILE: DXMainClient/Resources/DTA/GenericWindow.ini ================================================ [GenericWindow] BackgroundTexture=MainMenu/dbak.png DrawMode=Centered ;Stretched DrawBorders=false [LoadingScreen] Size=1280,720 [GameCreationWindow] BackgroundTexture=MainMenu/dbak.png Size=490,205 DrawMode=Centered ;Stretched DrawBorders=false [ExtraControls] 00=bar_ul:XNAExtraPanel 01=bar_ur:XNAExtraPanel 02=bar_lr:XNAExtraPanel 03=bar_ll:XNAExtraPanel 04=rightbar:XNAExtraPanel 05=leftbar:XNAExtraPanel 06=glow_t:XNAExtraPanel 07=glow_b:XNAExtraPanel 08=glow_l:XNAExtraPanel 09=glow_r:XNAExtraPanel 10=glow_tl:XNAExtraPanel 11=glow_tr:XNAExtraPanel 12=glow_bl:XNAExtraPanel 13=glow_br:XNAExtraPanel [bar_ul] Location=-24,0 BackgroundTexture=bar_ul.png [bar_ur] BackgroundTexture=bar_ur.png Location=0,0 DistanceFromRightBorder=-24 ; overrides the Location= key's X-coordinate [bar_lr] BackgroundTexture=bar_lr.png DistanceFromRightBorder=-24 DistanceFromBottomBorder=0 [bar_ll] Location=-24,0 BackgroundTexture=bar_ll.png DistanceFromBottomBorder=0 [rightbar] Location=0,12 BackgroundTexture=rightbar.png DistanceFromRightBorder=-24 FillHeight=12 [leftbar] Location=-24,12 BackgroundTexture=leftbar.png FillHeight=12 [glow_t] RemapColor=192,192,192,192 ;extra transparency Location=16,0 BackgroundTexture=glow_t.png FillWidth=16 [glow_b] RemapColor=192,192,192,192 ;extra transparency Location=16,0 BackgroundTexture=glow_b.png DistanceFromBottomBorder=0 FillWidth=16 [glow_l] RemapColor=192,192,192,192 ;extra transparency Location=0,16 BackgroundTexture=glow_l.png FillHeight=16 [glow_r] RemapColor=192,192,192,192 ;extra transparency Location=0,16 BackgroundTexture=glow_r.png DistanceFromRightBorder=0 FillHeight=16 [glow_tl] RemapColor=192,192,192,192 ;extra transparency Location=0,0 BackgroundTexture=glow_tl.png [glow_tr] RemapColor=192,192,192,192 ;extra transparency Location=0,0 BackgroundTexture=glow_tr.png DistanceFromRightBorder=0 [glow_bl] RemapColor=192,192,192,192 ;extra transparency Location=0,0 BackgroundTexture=glow_bl.png DistanceFromBottomBorder=0 [glow_br] RemapColor=192,192,192,192 ;extra transparency Location=0,0 BackgroundTexture=glow_br.png DistanceFromBottomBorder=0 DistanceFromRightBorder=0 [PrivacyNotification] BackgroundTexture=MainMenu/dbak.png DrawMode=Centered FillWidth=48 Location=24,0 DistanceFromBottomBorder=0 ;Width=920 ;FillWidth=48 ;Location=180,317 ;24,317 DrawBorders=false [btnOK] DistanceFromRightBorder=25 [SkirmishLobby] ;DrawMode=Tiled $Width=RESOLUTION_WIDTH - 40 ; Defines the width of the window $Height=RESOLUTION_HEIGHT - 34 ; Defines the height of the window DrawBorders=false ;[MultiplayerGameLobby] is controlled by $BaseSection=SkirmishLobby in LANGameLobby.ini [$ExtraControls] ;$CCbg=Background:XNAPanel $CCbar_ul=winbar_ul:XNAPanel $CCbar_ur=winbar_ur:XNAPanel $CCbar_lr=winbar_lr:XNAPanel $CCbar_ll=winbar_ll:XNAPanel $CCbar_l=winbar_r:XNAPanel $CCbar_r=winbar_l:XNAPanel $CCglow_tl=glow_top_left:XNAPanel $CCglow_tr=glow_top_right:XNAPanel $CCglow_bl=glow_bottom_left:XNAPanel $CCglow_br=glow_bottom_right:XNAPanel $CCglow_t=glow_top:XNAPanel $CCglow_b=glow_bottom:XNAPanel $CCglow_l=glow_left:XNAPanel $CCglow_r=glow_right:XNAPanel [Background] DrawMode=Tiled DrawBorders=false $Width=getWidth($ParentControl) $Height=getHeight($ParentControl) + 4 $X=0 $Y=-2 DrawOrder=-2000 UpdateOrder=-2000 [winbar_ul] BackgroundTexture=bar_ul.png $Width=24 $Height=12 DrawBorders=false $X=- (getWidth($Self) - 4) $Y=0 [winbar_ur] BackgroundTexture=bar_ur.png $Width=24 $Height=12 DrawBorders=false $X=getWidth($ParentControl) - 4 $Y=0 [winbar_lr] BackgroundTexture=bar_lr.png $Width=24 $Height=12 DrawBorders=false $X=getWidth($ParentControl) - 4 $Y=getHeight($ParentControl) - getHeight($Self) [winbar_ll] BackgroundTexture=bar_ll.png $Width=24 $Height=12 DrawBorders=false $X=- (getWidth($Self) - 4) $Y=getHeight($ParentControl) - getHeight($Self) [winbar_r] BackgroundTexture=rightbar.png DrawMode=Tiled $Width=24 $Height=getHeight($ParentControl) - (getHeight(winbar_ur) + getHeight(winbar_lr)) DrawBorders=false $X=getWidth($ParentControl) - 4 $Y=getHeight(winbar_ur) [winbar_l] BackgroundTexture=leftbar.png DrawMode=Tiled $Width=24 $Height=getHeight($ParentControl) - (getHeight(winbar_ul) + getHeight(winbar_ll)) DrawBorders=false $X=- (getWidth($Self) - 4) $Y=getHeight(winbar_ul) [glow_top_left] BackgroundTexture=glow_tl.png RemapColor=192,192,192,192 ;extra transparency $Width=16 $Height=16 DrawBorders=false $X=4 $Y=0 [glow_top_right] BackgroundTexture=glow_tr.png RemapColor=192,192,192,192 ;extra transparency $Width=16 $Height=16 DrawBorders=false $X=4 $X=getWidth($ParentControl) - getWidth($Self) - 4 [glow_bottom_left] BackgroundTexture=glow_bl.png RemapColor=192,192,192,192 ;extra transparency $Width=16 $Height=16 DrawBorders=false $X=4 $Y=getHeight($ParentControl) - getHeight($Self) [glow_bottom_right] BackgroundTexture=glow_br.png RemapColor=192,192,192,192 ;extra transparency $Width=16 $Height=16 DrawBorders=false $X=getWidth($ParentControl) - getWidth($Self) - 4 $Y=getHeight($ParentControl) - getHeight($Self) [glow_top] BackgroundTexture=glow_t.png RemapColor=192,192,192,192 ;extra transparency $Width=getWidth($ParentControl) - (getWidth(glow_top_left) + getWidth(glow_top_right)) - 8 $Height=16 DrawBorders=false $X=getWidth(glow_top_left) + 4 $Y=0 [glow_bottom] BackgroundTexture=glow_b.png RemapColor=192,192,192,192 ;extra transparency $Width=getWidth($ParentControl) - (getWidth(glow_bottom_left) + getWidth(glow_bottom_right)) - 8 $Height=16 DrawBorders=false $X=getWidth(glow_top_left) + 4 $Y=getHeight($ParentControl) - getHeight($Self) [glow_left] BackgroundTexture=glow_l.png RemapColor=192,192,192,192 ;extra transparency $Width=16 $Height=getHeight($ParentControl) - (getHeight(glow_top_left) + getHeight(glow_bottom_left)) DrawBorders=false $X=4 $Y=getHeight(glow_top_left) [glow_right] BackgroundTexture=glow_r.png RemapColor=192,192,192,192 ;extra transparency $Width=16 $Height=getHeight($ParentControl) - (getHeight(glow_top_right) + getHeight(glow_bottom_right)) DrawBorders=false $X=getWidth($ParentControl) - getWidth($Self) - 4 $Y=getHeight(glow_top_right) ================================================ FILE: DXMainClient/Resources/DTA/KeyboardCommands.ini ================================================ ; Dawn of the Tiberium Age (DTA) CnCNet Client ; In-Game Keyboard Commands ; https://github.com/CnCNet/xna-cncnet-client ; If you use or redistribute the client in any public projects, please include ; Rampastring and Dawn of the Tiberium Age in your project's credits. [ChatToAllies] UIName=Chat to allies Category=Multiplayer Description=Chat to players in your team. DefaultKey=8 [ChatToAll] UIName=Chat to everyone Category=Multiplayer Description=Chat to all players in the game (same as F8). DefaultKey=13 [GrantControl] UIName=Grant Control Category=Control Description=Give control of your units to the owner of a selected object. DefaultKey=0 [SelectOneLess] UIName=Select One Unit Less Category=Control Description=Randomly unselect one of your selected units. DefaultKey=0 [ToggleRadar] UIName=Radar Toggle Category=Interface Description=Toggle between the radar and the kill count screen (multiplayer only). DefaultKey=9 [ScreenCapture] UIName=Screen Capture Category=Interface Description=Takes a screenshot and saves it to the "Screenshots" sub-directory in your game directory. DefaultKey=579 [ToggleInfoPanel] UIName=Toggle Info Panel Category=Sidebar ;was Interface Description=Toggles the state of the sidebar info panel. DefaultKey=192 [ShowHelp] UIName=Show Key Commands Category=Interface Description=Displays an overview of important keyboard commands. DefaultKey=20 [PlaceBuilding] UIName=Place Building Category=Interface Description=Places a finished building. DefaultKey=90 [RepeatBuilding] UIName=Repeat Last Building Category=Interface Description=Repeats the last finished building. DefaultKey=602 [TogglePower] UIName=Power Mode Category=Interface Description=Enable power mode (allows powering structures on and off). DefaultKey=80 [ToggleRepair] UIName=Repair Mode Category=Interface Description=Enable repair mode. DefaultKey=82 [ToggleSell] UIName=Sell Mode Category=Interface Description=Enable sell mode. DefaultKey=594 [WaypointMode] UIName=Waypoint Mode Category=Interface Description=Enable waypoint mode. DefaultKey=87 [DeleteWaypoint] UIName=Delete Waypoint Category=Interface Description=Deletes a waypoint. DefaultKey=110 [ScrollNorth] UIName=Scroll North Category=Interface Description=Scroll the camera towards the north. DefaultKey=2086 [ScrollSouth] UIName=Scroll South Category=Interface Description=Scroll the camera towards the south. DefaultKey=2088 [ScrollEast] UIName=Scroll East Category=Interface Description=Scroll the camera towards the east. DefaultKey=2087 [ScrollWest] UIName=Scroll West Category=Interface Description=Scroll the camera towards the west. DefaultKey=2085 [LeftSidebarUp] UIName=Structure List Up Category=Sidebar Description=Scroll the sidebar's structure list up. DefaultKey=36 [RightSidebarUp] UIName=Unit List Up Category=Sidebar Description=Scroll the sidebar's unit list up. DefaultKey=33 [SidebarPageUp] UIName=Sidebar Page Up Category=Sidebar Description=Scroll the sidebar up by a page. DefaultKey=0 [LeftSidebarPageUp] UIName=Structure List Page Up Category=Sidebar Description=Scroll the sidebar's structure list up by a page. DefaultKey=0 [RightSidebarPageUp] UIName=Unit List Page Up Category=Sidebar Description=Scroll the sidebar's unit list up by a page. DefaultKey=0 [LeftSidebarDown] UIName=Structure List Down Category=Sidebar Description=Scroll the sidebar's structure list down. DefaultKey=35 [RightSidebarDown] UIName=Unit List Down Category=Sidebar Description=Scroll the sidebar's unit list down. DefaultKey=34 [SidebarPageDown] UIName=Sidebar Page Down Category=Sidebar Description=Scroll the sidebar down by a page. DefaultKey=0 [LeftSidebarPageDown] UIName=Structure List Page Down Category=Sidebar Description=Scroll the sidebar's structure list down by a page. DefaultKey=0 [RightSidebarPageDown] UIName=Unit List Page Down Category=Sidebar Description=Scroll the sidebar's unit list down by a page. DefaultKey=0 [SidebarUp] UIName=Sidebar Up Category=Sidebar Description=Scroll the sidebar up. DefaultKey=0 [SidebarDown] UIName=Sidebar Down Category=Sidebar Description=Scroll the sidebar down. DefaultKey=0 [SelectType] UIName=Select Same Type Category=Selection Description=Select all units on the screen that are the type of your currently selected units. DefaultKey=84 [SelectView] UIName=Select View Category=Selection Description=Select all units on the screen. DefaultKey=69 [NextObject] UIName=Next Unit Category=Selection Description=Select the next unit. DefaultKey=78 [PreviousObject] UIName=Previous Unit Category=Selection Description=Select the previous unit. DefaultKey=66 [CenterView] UIName=Center View Category=Interface Description=Center the camera to the selected objects. DefaultKey=12 [Options] UIName=Options Menu Category=Interface Description=Open the in-game Options menu. DefaultKey=27 [CenterBase] UIName=Center Base Category=Interface Description=Center the camera on your base. DefaultKey=72 [Follow] UIName=Follow Category=Interface Description=Make the selected objects follow another object. DefaultKey=70 [View1] UIName=View Bookmark 1 Category=Interface Description=Center the camera on bookmark 1. DefaultKey=120 [View2] UIName=View Bookmark 2 Category=Interface Description=Center the camera on bookmark 2. DefaultKey=121 [View3] UIName=View Bookmark 3 Category=Interface Description=Center the camera on bookmark 3. DefaultKey=122 [View4] UIName=View Bookmark 4 Category=Interface Description=Center the camera on bookmark 4. DefaultKey=123 [SetView1] UIName=Set Bookmark 1 Category=Interface Description=Sets bookmark 1. DefaultKey=632 [SetView2] UIName=Set Bookmark 2 Category=Interface Description=Sets bookmark 2. DefaultKey=633 [SetView3] UIName=Set Bookmark 3 Category=Interface Description=Sets bookmark 3. DefaultKey=634 [SetView4] UIName=Set Bookmark 4 Category=Interface Description=Sets bookmark 4. DefaultKey=635 [CenterOnRadarEvent] UIName=Goto Radar Event Category=Interface Description=Center the camera around the latest radar event. DefaultKey=32 [ToggleAlliance] UIName=Alliance Category=Control Description=Form an alliance with the owner of a selected object. DefaultKey=65 [DeployObject] UIName=Deploy Object Category=Control Description=Deploy selected units. DefaultKey=68 [GuardObject] UIName=Guard Category=Control Description=Make your selected units guard the nearby area and automatically attack enemies. DefaultKey=71 [ScatterObject] UIName=Scatter Category=Control Description=Make your selected units scatter. DefaultKey=88 [StopObject] UIName=Stop Object Category=Control Description=Stop your selected units. DefaultKey=83 [AllToCheer] UIName=Cheer Category=Control Description=Make all of your infantry units cheer. DefaultKey=0 [TeamAddSelect_1] UIName=Add Select Team 1 Category=Team Description=Select team 1 without unselecting already selected objects DefaultKey=305 [TeamAddSelect_2] UIName=Add Select Team 2 Category=Team Description=Select team 2 without unselecting already selected objects DefaultKey=306 [TeamAddSelect_3] UIName=Add Select Team 3 Category=Team Description=Select team 3 without unselecting already selected objects DefaultKey=307 [TeamAddSelect_4] UIName=Add Select Team 4 Category=Team Description=Select team 4 without unselecting already selected objects DefaultKey=308 [TeamAddSelect_5] UIName=Add Select Team 5 Category=Team Description=Select team 5 without unselecting already selected objects DefaultKey=309 [TeamAddSelect_6] UIName=Add Select Team 6 Category=Team Description=Select team 6 without unselecting already selected objects DefaultKey=310 [TeamAddSelect_7] UIName=Add Select Team 7 Category=Team Description=Select team 7 without unselecting already selected objects DefaultKey=311 [TeamAddSelect_8] UIName=Add Select Team 8 Category=Team Description=Select team 8 without unselecting already selected objects DefaultKey=312 [TeamAddSelect_9] UIName=Add Select Team 9 Category=Team Description=Select team 9 without unselecting already selected objects DefaultKey=313 [TeamAddSelect_10] UIName=Add Select Team 10 Category=Team Description=Select team 10 without unselecting already selected objects DefaultKey=304 [TeamCenter_1] UIName=Center Team 1 Category=Team Description=Center the camera around team 1 DefaultKey=1073 [TeamCenter_2] UIName=Center Team 2 Category=Team Description=Center the camera around team 2 DefaultKey=1074 [TeamCenter_3] UIName=Center Team 3 Category=Team Description=Center the camera around team 3 DefaultKey=1075 [TeamCenter_4] UIName=Center Team 4 Category=Team Description=Center the camera around team 4 DefaultKey=1076 [TeamCenter_5] UIName=Center Team 5 Category=Team Description=Center the camera around team 5 DefaultKey=1077 [TeamCenter_6] UIName=Center Team 6 Category=Team Description=Center the camera around team 6 DefaultKey=1078 [TeamCenter_7] UIName=Center Team 7 Category=Team Description=Center the camera around team 7 DefaultKey=1079 [TeamCenter_8] UIName=Center Team 8 Category=Team Description=Center the camera around team 8 DefaultKey=1080 [TeamCenter_9] UIName=Center Team 9 Category=Team Description=Center the camera around team 9 DefaultKey=1081 [TeamCenter_10] UIName=Center Team 10 Category=Team Description=Center the camera around team 10 DefaultKey=1072 [TeamCreate_1] UIName=Create Team 1 Category=Team Description=Creates team 1 DefaultKey=561 [TeamCreate_2] UIName=Create Team 2 Category=Team Description=Creates team 2 DefaultKey=562 [TeamCreate_3] UIName=Create Team 3 Category=Team Description=Creates team 3 DefaultKey=563 [TeamCreate_4] UIName=Create Team 4 Category=Team Description=Creates team 4 DefaultKey=564 [TeamCreate_5] UIName=Create Team 5 Category=Team Description=Creates team 5 DefaultKey=565 [TeamCreate_6] UIName=Create Team 6 Category=Team Description=Creates team 6 DefaultKey=566 [TeamCreate_7] UIName=Create Team 7 Category=Team Description=Creates team 7 DefaultKey=567 [TeamCreate_8] UIName=Create Team 8 Category=Team Description=Creates team 8 DefaultKey=568 [TeamCreate_9] UIName=Create Team 9 Category=Team Description=Creates team 9 DefaultKey=569 [TeamCreate_10] UIName=Create Team 10 Category=Team Description=Creates team 10 DefaultKey=560 [TeamSelect_1] UIName=Select Team 1 Category=Team Description=Selects team 1 DefaultKey=49 [TeamSelect_2] UIName=Select Team 2 Category=Team Description=Selects team 2 DefaultKey=50 [TeamSelect_3] UIName=Select Team 3 Category=Team Description=Selects team 3 DefaultKey=51 [TeamSelect_4] UIName=Select Team 4 Category=Team Description=Selects team 4 DefaultKey=52 [TeamSelect_5] UIName=Select Team 5 Category=Team Description=Selects team 5 DefaultKey=53 [TeamSelect_6] UIName=Select Team 6 Category=Team Description=Selects team 6 DefaultKey=54 [TeamSelect_7] UIName=Select Team 7 Category=Team Description=Selects team 7 DefaultKey=55 [TeamSelect_8] UIName=Select Team 8 Category=Team Description=Selects team 8 DefaultKey=56 [TeamSelect_9] UIName=Select Team 9 Category=Team Description=Selects team 9 DefaultKey=57 [TeamSelect_10] UIName=Select Team 10 Category=Team Description=Selects team 10 DefaultKey=48 ================================================ FILE: DXMainClient/Resources/DTA/LANGameLobby.ini ================================================ [INISystem] BasedOn=MultiplayerGameLobby.ini ================================================ FILE: DXMainClient/Resources/DTA/LANLobby.ini ================================================ [INISystem] BasedOn=GenericWindow.ini [lblColor] Location=300,14 [ddColor] ;Location=395,12 [lblCurrentChannel] DistanceFromRightBorder=431 [ddCurrentChannel] DistanceFromRightBorder=212 [lbGameList] Location=12,43 FillHeight=45 [lbPlayerList] Location=0,43 DistanceFromRightBorder=12 FillHeight=45 [lbChatMessages] Location=300,43 FillWidth=212 FillHeight=45 [tbChatInput] DistanceFromBottomBorder=12 FillWidth=212 [btnLogout] DistanceFromRightBorder=12 DistanceFromBottomBorder=12 [btnNewGame] DistanceFromBottomBorder=12 [btnJoinGame] DistanceFromBottomBorder=12 [ExtraControls] L1=btnModDB:XNALinkButton L2=btnForums:XNALinkButton [btnModDB] IdleTexture=moddbInactive.png HoverTexture=moddbActive.png URL=http://www.moddb.com/mods/tiberian-sun-client Size=21,21 Location=0,12 DistanceFromRightBorder=180 DrawOrder=1 [btnForums] IdleTexture=forumsInactive.png HoverTexture=forumsActive.png URL=https://ppmforums.com/index.php?f=24 Size=21,21 Location=0,12 DistanceFromRightBorder=159 DrawOrder=1 ================================================ FILE: DXMainClient/Resources/DTA/LoadingScreen.ini ================================================ [LoadingScreen] Size=1280,720 BackgroundTexture=lsbg.png DrawBorders=false [ExtraControls] 0=logo:XNAExtraPanel 1=text:XNAExtraPanel 2=legal:XNAExtraPanel [logo] Location=754,15 BackgroundTexture=lslogo.png RepeatingImage=false Size=523,318 [text] Location=329,576 BackgroundTexture=lstext.png RepeatingImage=false Size=198,32 [legal] BackgroundTexture=ts_legal_text.png RepeatingImage=false Size=815,114 DistanceFromRightBorder=-9 DistanceFromBottomBorder=-10 ================================================ FILE: DXMainClient/Resources/DTA/MainMenu.ini ================================================ [MainMenu] Size=1280,720 DrawBorders=false [MainMenuUIPanel] DrawBorders=false [ExtraControls] 0=Logo:XNAExtraPanel 1=txtVersion:XNALabel 2=btnRankedMatch:XNALinkButton [Logo] Location=369,8 BackgroundTexture=MainMenu/Logo.png [btnNewCampaign] Location=490,261 IdleTexture=MainMenu/campaign.png [btnLoadGame] Location=490,315 IdleTexture=MainMenu/loadmission.png [btnCnCNet] Location=490,369 IdleTexture=MainMenu/playonline.png HoverTexture=MainMenu/playonline_c.png [btnRankedMatch] Location=490,423 IdleTexture=MainMenu/rankedmatch.png HoverTexture=MainMenu/rankedmatch_c.png HoverSoundEffect=MainMenu/button.wav URL=CnCNetQM.exe Enabled=false [btnLan] Location=490,477 IdleTexture=MainMenu/lan.png [btnSkirmish] Location=490,531 IdleTexture=MainMenu/skirmish.png [btnExtras] Location=490,531 Visible=false IdleTexture=MainMenu/extras.png [btnOptions] Location=490,585 IdleTexture=MainMenu/options.png [btnExit] Location=145,635 IdleTexture=MainMenu/exitgame.png [btnCredits] Location=565,668 IdleTexture=MainMenu/credits.png HoverTexture=MainMenu/credits_c.png Text= HoverSoundEffect=MainMenu/button.wav [btnStatistics] Location=415,668 IdleTexture=MainMenu/statistics.png HoverTexture=MainMenu/statistics_c.png Text= HoverSoundEffect=MainMenu/button.wav [btnMapEditor] Location=715,668 IdleTexture=MainMenu/mapeditor.png HoverTexture=MainMenu/mapeditor_c.png Text= HoverSoundEffect=MainMenu/button.wav Enabled=false [lblCnCNetStatus] Text=TS Players on CnCNet RemapColor=45,228,255 Location=1099,70 DistanceFromRightBorder=159 [lblCnCNetPlayerCount] RemapColor=45,228,255 Location=990,70 DistanceFromRightBorder=296 [txtVersion] Text=Version: RemapColor=45,228,255 Location=990,91 DistanceFromRightBorder=262 [lblVersion] RemapColor=45,228,255 IdleColor=45,228,255 HoverColor=255,255,255 Location=1304,91 DistanceFromRightBorder=256 [lblUpdateStatus] RemapColor=45,228,255 IdleColor=45,228,255 HoverColor=255,255,255 Location=990,112 DistanceFromRightBorder=139 ================================================ FILE: DXMainClient/Resources/DTA/MultiplayerGameLobby.ini ================================================ [INISystem] BasedOn=GameLobbyBase.ini [GameOptionsPanel] $CC18=chkIngameAllying:GameLobbyCheckBox [MultiplayerGameLobby] $BaseSection=SkirmishLobby PlayerStatusIndicatorX=3 PlayerStatusIndicatorY=1 PlayerOptionLocationX=22 ;def=25 PlayerNameWidth=117; def=136 $CCMP01=btnLockGame:XNAClientButton $CCMP02=lbChatMessages:ChatListBox $CCMP03=lbChatMessages_Player:ChatListBox $CCMP04=tbChatInput:XNAChatTextBox $CCMP05=tbChatInput_Player:XNAChatTextBox $CCMP06=chkAutoSave:GameLobbyCheckBox $CCMP07=chkAutoReady:XNAClientCheckBox [cmbGameSpeedCap] Items=60 FPS,52 FPS,45 FPS,40 FPS,30 FPS,20 FPS,15 FPS DefaultIndex=0 [btnLockGame] Text=Lock Game $X=getRight(btnLaunchGame) + BUTTON_SPACING $Y=getY(btnLaunchGame) $Width=133 [btnLeaveGame] Text=Leave Game [chkAutoSave] Text=Auto Save $X=getRight(btnLockGame) + BUTTON_SPACING $Y=getY(btnLaunchGame) + 3 [chkAutoReady] Text=Auto Accept $X=getRight(chkAutoSave) + BUTTON_SPACING $Y=getY(btnLaunchGame) + 3 [lbMapList] $Height=getHeight(MapPreviewBox) $Y=getY(MapPreviewBox) [lbChatMessages] SolidColorBackgroundTexture=0,0,0,192 $X=EMPTY_SPACE_SIDES $Y=EMPTY_SPACE_TOP $Width=getWidth($ParentControl) - (getX($Self) + (getWidth(MapPreviewBox) + EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING) $Height=getHeight(GameOptionsPanel) - 21 - LOBBY_PANEL_SPACING ;235 [lbChatMessages_Player] BaseSection=lbChatMessages $Height=getBottom(MapPreviewBox) - getY($Self) - 21 - LOBBY_PANEL_SPACING [tbChatInput] Suggestion=Type here to chat... $Width=getWidth(lbChatMessages) $Height=21 $X=getX(lbChatMessages) $Y=getBottom(lbChatMessages) + LOBBY_PANEL_SPACING [tbChatInput_Player] Suggestion=Type here to chat... $Width=getWidth(lbChatMessages_Player) $Height=21 $X=getX(lbChatMessages_Player) $Y=getBottom(lbChatMessages_Player) + LOBBY_PANEL_SPACING [chkMultiEng] Checked=True [chkCrates] Checked=False [chkIngameAllying] Enabled=True ================================================ FILE: DXMainClient/Resources/DTA/OptionsWindow.ini ================================================ [INISystem] BasedOn=GenericWindow.ini [DisplayOptionsPanelExtraControls] 0=chkStretchMovies:SettingCheckBox 1=chkMEDDraw:FileSettingCheckBox 2=lblReShade:XNALabel 3=ddReShade:FileSettingDropDown [chkMEDDraw] Location=12,217 ;285,216 Text=Enable DDWrapper for Map Editor ToolTip=Enables DirectDraw wrapper & emulation for map editor.@Turning this option on can help if you are encountering problems with editor viewport not displaying or being laggy. EnabledFile0=Resources/Compatibility/DLL/ddwrapper.dll,Map Editor/ddraw32.dll,AlwaysOverwrite_LinkAsReadOnly EnabledFile1=Resources/Compatibility/Configs/aqrit.cfg,Map Editor/aqrit.cfg,KeepChanges [lblReShade] Text=ReShade Shaders: ToolTip=Use ReShade shaders to enhance graphics (Warning: GPU intensive)@Only works with TS-DDRAW, TS-DDRAW-2 and CNC-DDRAW.@DX11/OpenGL should work for most users, if no ReShade message is shown when loading game, use DX9. Location=13,246 [ddReShade] Location=140,246 ;161 Size=120,21 ;133,21 Items=Disabled,Enabled - DX11,Enabled - DX9,Enabled - OpenGL ToolTip=Use ReShade shaders to enhance graphics (Warning: GPU intensive)@Only works with TS-DDRAW, TS-DDRAW-2 and CNC-DDRAW.@DX11/OpenGL should work for most users, if no ReShade message is shown when loading game, use DX9. DefaultValue=0 CheckFilePresence=yes ResetUnselectableItem=yes ForceApplyUnselectableItem=no RestartRequired=false Item1File0=Resources/ReShade Files/dxgi.dll,dxgi.dll,AlwaysOverwrite_LinkAsReadOnly Item1File1=Resources/ReShade Files/ReShade.ini,ReShade.ini Item2File0=Resources/ReShade Files/d3d9.dll,d3d9.dll,AlwaysOverwrite_LinkAsReadOnly Item2File1=Resources/ReShade Files/ReShade.ini,ReShade.ini Item3File0=Resources/ReShade Files/opengl32.dll,opengl32.dll,AlwaysOverwrite_LinkAsReadOnly Item3File1=Resources/ReShade Files/ReShade.ini,ReShade.ini [lblDetailLevel] ToolTip=Select the level of detail. Lower levels will reduce visual effects and increase performance. [ddDetailLevel] ToolTip=Select the level of detail. Lower levels will reduce visual effects and increase performance. [lblRenderer] ToolTip=Select the DDraw wrapper to use. If you experience graphical or performance issues, try a different wrapper. [ddRenderer] ToolTip=Select the DDraw wrapper to use. If you experience graphical or performance issues, try a different wrapper. [chkBackBufferInVRAM] Text=Back Buffer in Video Memory ;Here I moved the explanation to the tooltip ToolTip=Enable back buffer in VRAM. Reduces performance, but is necessary on some systems. [chkScrollCoasting] ToolTip=Enable smooth scrolling. [chkTargetLines] ToolTip=Show lines between selected units and targets.@Green lines indicate movement, red lines attack. [chkTooltips] ToolTip=Enable in-game tooltips. [chkBlackChatBackground] Text=Dark Chat Background ToolTip=Use black background for in-game chat messages. [chkAltToUndeploy] Text=Hold Alt to Undeploy ToolTip=Undeploy units by holding the [Alt] key while giving a move command. [chkStretchMovies] Location=12,196 Text=Stretch Videos SettingSection=Video SettingKey=StretchMovies [chkStopMusicOnMenu] Location=12,221 [btnSave] DistanceFromRightBorder=124 ================================================ FILE: DXMainClient/Resources/DTA/ReShade Files/ReShade.ini ================================================ [D3D9] DepthCopyAtClearIndex=0 DepthCopyBeforeClears=0 DisableINTZ=0 UseAspectRatioHeuristics=1 [DX11_BUFFER_DETECTION] DepthBufferClearingNumber=0 DepthBufferRetrievalMode=0 UseAspectRatioHeuristics=1 [DX9_BUFFER_DETECTION] DisableINTZ=0 PreserveDepthBuffer=0 PreserveDepthBufferIndex=4294967295 UseAspectRatioHeuristics=1 [GENERAL] ClockFormat=0 CurrentPresetPath=.\reshade-shaders\RotE_Xtended.ini EffectSearchPaths=.\reshade-shaders\Shaders,.\reshade-shaders\Shaders\Pirate,.\reshade-shaders\Shaders\qUINT,.\reshade-shaders\Shaders\PD80,.\reshade-shaders\Shaders\Depth3D,.\reshade-shaders\Shaders\Daodan FPSPosition=1 NewVariableUI=0 NoDebugInfo=0 NoFontScaling=1 NoReloadOnInit=0 PerformanceMode=1 PreprocessorDefinitions=RESHADE_DEPTH_LINEARIZATION_FAR_PLANE=1000.0,RESHADE_DEPTH_INPUT_IS_UPSIDE_DOWN=0,RESHADE_DEPTH_INPUT_IS_REVERSED=0,RESHADE_DEPTH_INPUT_IS_LOGARITHMIC=0 PresetPath=.\reshade-shaders\RotE_Xtended.ini PresetTransitionDelay=1000 SaveWindowState=0 ScreenshotFormat=1 ScreenshotIncludePreset=0 ScreenshotPath= ScreenshotSaveBefore=0 ScreenshotSaveUI=0 ShowClock=0 ShowFPS=0 ShowFrameTime=0 ShowScreenshotMessage=1 SkipLoadingDisabledEffects=0 TextureSearchPaths=.\reshade-shaders\Textures TutorialProgress=4 [INPUT] ForceShortcutModifiers=1 InputProcessing=2 KeyEffects=0,0,0,0 KeyMenu=36,0,0,0 KeyNextPreset=0,0,0,0 KeyOverlay=36,0,0,0 KeyPerformanceMode=0,0,0,0 KeyPreviousPreset=0,0,0,0 KeyReload=0,0,0,0 KeyScreenshot=44,0,0,0 [OPENGL] ForceMainDepthBuffer=0 UseAspectRatioHeuristics=1 [OVERLAY] ClockFormat=0 FPSPosition=1 NoFontScaling=0 SaveWindowState=0 ShowClock=0 ShowForceLoadEffectsButton=1 ShowFPS=0 ShowFrameTime=0 ShowScreenshotMessage=1 TutorialProgress=2 VariableListHeight=300.000000 VariableListUseTabs=0 [SCREENSHOTS] ClearAlpha=1 FileFormat=1 FileNamingFormat=0 JPEGQuality=90 SaveBeforeShot=0 SaveOverlayShot=0 SavePath= SavePresetFile=0 [STYLE] Alpha=1.000000 ChildRounding=0.000000 ColFPSText=1.000000,1.000000,0.784314,1.000000 EditorFont= EditorFontSize=13 EditorStyleIndex=0 Font= FontSize=13 FPSScale=1.000000 FrameRounding=0.000000 GrabRounding=0.000000 PopupRounding=0.000000 ScrollbarRounding=0.000000 StyleIndex=2 TabRounding=4.000000 WindowRounding=0.000000 ================================================ FILE: DXMainClient/Resources/DTA/Renderers.ini ================================================ ; DTA CnCNet Client Renderers.ini ; Specifies the available DirectDraw wrappers in the client's options menu. [Renderers] 0=CnC-DDRAW 1=DDrawCompat 2=TS-DDRAW-OPENGL 3=TS-DDRAW-GDI 4=Software 5=Default ; Specifies the default renderers for different operating systems. [DefaultRenderer] UNKNOWN=Default WINXP=Default WINVISTA=CnC-DDRAW WIN7=CnC-DDRAW WIN810=CnC-DDRAW UNIX=Default ; Renderer sections start below. ; The main ddraw.dll for a renderer is specified in DLLName=. ; The file is expected to be found from the Resources\ directory, and it is ; copied to the game directory as ddraw.dll when settings are saved. ; AdditionalFiles= is a comma-separated list of additional files to be copied ; to the game directory. The client also expects to find them from the Resources\ ; directory, and copies them to the main directory when settings are saved. ; ConfigFilePath= works similarly. The only difference is that if the config ; file already exists, it is not overwritten (the DLLs and additional files are). ; You can also specify sub-directories in the Resources\ directory for the paths. ; For example, if you specify DLLName=Renderers\my_awesome_wrapper.dll, the client ; expects to find the file from \Resources\Renderers\my_awesome_wrapper.dll. ; When settings are saved, it is still copied to the root of the main game directory. [Default] UIName=Stock [TS-DDRAW-OPENGL] UIName=TS-DDRAW (OGL) DLLName=ts_ddraw.dll ;ts-ddraw-opengl.dll ResConfigFileName=ts-ddraw.ini ConfigFileName=ddraw.ini UseQres=No SingleCoreAffinity=false [TS-DDRAW-GDI] UIName=TS-DDRAW (GDI) DLLName=ts_ddraw.dll ;ts-ddraw-gdi.dll ResConfigFileName=ts-ddraw-gdi.ini ConfigFileName=ddraw.ini UseQres=No SingleCoreAffinity=false [CnC-DDRAW] UIName=CnC-DDRAW DLLName=cnc-ddraw.dll ResConfigFileName=cnc-ddraw.ini ConfigFileName=ddraw.ini UseQres=No WindowedModeSection=ddraw WindowedModeKey=windowed BorderlessWindowedModeKey=border IsBorderlessWindowedModeKeyReversed=true [Software] UIName=Software DLLName=ddraw_nohw.dll DisallowedOperatingSystems=WINVISTA,WIN7,WIN810 [DDrawCompat] UIName=DDrawCompat DLLName=ddrawcompat.dll ResConfigFileName=ddrawcompat.ini ConfigFileName=ddraw.ini ================================================ FILE: DXMainClient/Resources/DTA/SkirmishLobby.ini ================================================ [INISystem] BasedOn=GameLobbyBase.ini ================================================ FILE: DXMainClient/Resources/DTA/StatisticsWindow.ini ================================================ [INISystem] BasedOn=GenericWindow.ini [panelGameStatistics] SolidColorBackgroundTexture=0,0,0,32 [panelTotalStatistics] SolidColorBackgroundTexture=0,8,0,128 [btnReturnToMenu] DistanceFromRightBorder=12 [btnClearStatistics] Visible=true ================================================ FILE: DXMainClient/Resources/DTA/UpdaterConfig.ini ================================================ [Settings] ; Comma-separated list of filename masks that are exempted from the file integrity checks. ; Overrides a following built-in list: .rtf,.txt,Theme.ini,gui_settings.xml IgnoreMasks=.rtf, .txt, Theme.ini, gui_settings.xml, .htm, html, .doc, .xdoc, .log ; List of update download mirrors. ; Format: URL,UI Name,Location. ; Example: 0=http://testurl/updates/,Test Update Server,Europe ; Location is optional. [DownloadMirrors] 0=https://downloads.cncnet.org/updates/games/ts/live/,CnCNet,Europe ; List of custom components. ; Format: UI Name, Version File ID, Download File Path, Local File Path ; Example: 0=Test Component, TESTCOMPONENT, Test/testfile.mix, Test\testfile.mix [CustomComponents] 0=Tiberian Sun Music,TSMUSIC,MIX/SCORES.MIX,MIX\SCORES.MIX 1=Firestorm Music,FSMUSIC,MIX/SCORES01.MIX,MIX\SCORES01.MIX 2=Tiberian Sun GDI Campaign Videos,MOVIES01,MIX/Movies01.mix,MIX\Movies01.mix 3=Tiberian Sun Nod Campaign Videos,MOVIES02,MIX/Movies02.mix,MIX\Movies02.mix 4=Firestorm Campaign Videos,MOVIES03,MIX/Movies03.mix,MIX\Movies03.mix ================================================ FILE: DXMainClient/Resources/DTA/UserDefaults.ini ================================================ [Video] IntegerScaledClient=True BorderlessWindowedClient=False [Audio] PlayMainMenuMusic=False [Options] WriteInstallationPathToRegistry=False CheckforUpdates=False ================================================ FILE: DXMainClient/Resources/DTA/ZLIB.License.txt ================================================ The following licenses govern use of the accompanying software, the DotNetZip library ("the software"). If you use the software, you accept these licenses. If you do not accept the license, do not use the software. The managed ZLIB code included in Ionic.Zlib.dll and Ionic.Zip.dll is modified code, based on jzlib. The following notice applies to jzlib: ----------------------------------------------------------------------- Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of the authors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----------------------------------------------------------------------- jzlib is based on zlib-1.1.3. The following notice applies to zlib: ----------------------------------------------------------------------- Copyright (C) 1995-2004 Jean-loup Gailly and Mark Adler The ZLIB software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Jean-loup Gailly jloup@gzip.org Mark Adler madler@alumni.caltech.edu ----------------------------------------------------------------------- ================================================ FILE: DXMainClient/Resources/DTA/ZLIB.Ms-PL.txt ================================================ Microsoft Public License (Ms-PL) This license governs use of the accompanying software, the DotNetZip library ("the software"). If you use the software, you accept this license. If you do not accept the license, do not use the software. 1. Definitions The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. A "contribution" is the original software, or any additions or changes to the software. A "contributor" is any person that distributes its contribution under this license. "Licensed patents" are a contributor's patent claims that read directly on its contribution. 2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. 3. Conditions and Limitations (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. ================================================ FILE: DXMainClient/Resources/DTA/cnc-ddraw.ini ================================================ ; cnc-ddraw - https://github.com/FunkyFr3sh/cnc-ddraw [ddraw] ; ### Optional settings ### ; Use the following settings to adjust the look and feel to your liking ; Stretch to custom resolution, 0 = defaults to the size game requests width=0 height=0 ; Override the width/height settings shown above and always stretch to fullscreen ; Note: Can be combined with 'windowed=true' to get windowed-fullscreen aka borderless mode fullscreen=false ; Run in windowed mode rather than going fullscreen windowed=false ; Maintain aspect ratio maintas=false ; Windowboxing / Integer Scaling boxing=false ; Real rendering rate, -1 = screen rate, 0 = unlimited, n = cap ; Note: Does not have an impact on the game speed, to limit your game speed use 'maxgameticks=' maxfps=-1 ; Vertical synchronization, enable if you get tearing - (Requires 'renderer=auto/opengl*/direct3d9*') ; Note: vsync=true can fix tearing but it will cause input lag vsync=false ; Automatic mouse sensitivity scaling ; Note: Only works if stretching is enabled. Sensitivity will be adjusted according to the size of the window adjmouse=true ; Preliminary libretro shader support - (Requires 'renderer=opengl*') https://github.com/libretro/glsl-shaders ; 2x scaling example: https://imgur.com/a/kxsM1oY - 4x scaling example: https://imgur.com/a/wjrhpFV ; You can specify a full path to a .glsl shader file here or use one of the values listed below ; Possible values: Nearest neighbor, Bilinear, Bicubic, Lanczos, xBR-lv2 shader=Resources\Shaders\interpolation\catmull-rom-bilinear.glsl ; Window position, -32000 = center to screen posX=-32000 posY=-32000 ; Renderer, possible values: auto, opengl, openglcore, gdi, direct3d9, direct3d9on12 (auto = try direct3d9/opengl, fallback = gdi) renderer=auto ; Developer mode (don't lock the cursor) devmode=false ; Show window borders in windowed mode border=true ; Save window position/size/state on game exit and restore it automatically on next game start ; Possible values: 0 = disabled, 1 = save to global 'ddraw' section, 2 = save to game specific section savesettings=1 ; Should the window be resizable by the user in windowed mode? resizable=true ; Upscaling filter for the direct3d9* renderers ; Possible values: 0 = nearest-neighbor, 1 = bilinear, 2 = bicubic, 3 = lanczos (bicubic/lanczos only support 16/32bit color depth games) d3d9_filter=2 ; Enable upscale hack for high resolution patches (Supports C&C1, Red Alert 1 and KKND Xtreme) vhack=false ; Switch between windowed/borderless modes with alt+enter rather than windowed/fullscreen modes toggle_borderless=false ; Switch between windowed/fullscreen upscaled modes with alt+enter rather than windowed/fullscreen modes toggle_upscaled=false ; ### Compatibility settings ### ; Use the following settings in case there are any issues with the game ; Hide WM_ACTIVATEAPP and WM_NCACTIVATE messages to prevent problems on alt+tab noactivateapp=true ; Max game ticks per second, possible values: -1 = disabled, -2 = refresh rate, 0 = emulate 60hz vblank, 1-1000 = custom game speed ; Note: Can be used to slow down a too fast running game, fix flickering or too fast animations ; Note: Usually one of the following values will work: 60 / 30 / 25 / 20 / 15 (lower value = slower game speed) maxgameticks=0 ; Windows API Hooking, Possible values: 0 = disabled, 1 = IAT Hooking, 2 = Microsoft Detours, 3 = IAT+Detours Hooking (All Modules), 4 = IAT Hooking (All Modules) ; Note: Change this value if windowed mode or upscaling isn't working properly ; Note: 'hook=2' will usually work for problematic games, but 'hook=2' should be combined with renderer=gdi hook=4 ; Force minimum FPS, possible values: 0 = disabled, -1 = use 'maxfps=' value, -2 = same as -1 but force full redraw, 1-1000 = custom FPS ; Note: Set this to a low value such as 5 or 10 if some parts of the game are not being displayed (e.g. menus or loading screens) minfps=-1 ; Disable fullscreen-exclusive mode for the direct3d9*/opengl* renderers ; Note: Can be used in case some GUI elements like buttons/textboxes/videos/etc.. are invisible nonexclusive=false ; Force CPU0 affinity, avoids crashes/freezing, *might* have a performance impact ; Note: Disable this if the game is not running smooth or there are sound issues singlecpu=true ; Available resolutions, possible values: 0 = Small list, 1 = Very small list, 2 = Full list ; Note: Set this to 2 if your chosen resolution is not working or does not show up in the list ; Note: Set this to 1 if the game is crashing on startup resolutions=0 ; Child window handling, possible values: 0 = Disabled, 1 = Display top left, 2 = Display top left + repaint, 3 = Hide ; Note: Disables upscaling if a child window was detected (to ensure the game is fully playable, may look weird though) fixchilds=2 ; Enable one of the following settings if your cursor doesn't work properly when upscaling is enabled hook_peekmessage=false hook_getmessage=false ; Undocumented settings - You may or may not change these (You should rather focus on the settings above) tshack=true releasealt=false game_handles_close=false fixnotresponding=false guard_lines=200 max_resolutions=0 limit_bltfast=false lock_surfaces=false allow_wmactivate=false flipclear=false fixmousehook=false rgb555=false no_dinput_hook=false refresh_rate=0 anti_aliased_fonts_min_size=13 custom_width=0 custom_height=0 min_font_size=0 direct3d_passthrough=false ; ### Hotkeys ### ; Use the following settings to configure your hotkeys, 0x00 = disabled ; Virtual-Key Codes: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes ; Switch between windowed and fullscreen mode = [Alt] + [Enter] keytogglefullscreen=0x0D ; Maximize window without frame = [Alt] + [Page Down] keytogglemaximize=0x22 ; Unlock cursor 1 = [Ctrl] + [Tab] keyunlockcursor1=0x09 ; Unlock cursor 2 = [Right Alt] + [Right Ctrl] keyunlockcursor2=0xA3 ; Screenshot = [Prnt Scrn] keyscreenshot=0x2C ================================================ FILE: DXMainClient/Resources/DTA/ddrawcompat.ini ================================================ ColorKeyMethod = auto CpuAffinity = 1 DisplayFilter = point DpiAwareness = permonitor FpsLimiter = msgloop(120) RenderColorDepth = app SupportedDepthFormats = 16 SupportedResolutions = 640x400, native VSync = off WinVersionLie = 98 ================================================ FILE: DXMainClient/Resources/DTA/qres license.txt ================================================ QRes Source Code - Open Source License -------------------------------------- Copyright (c) 1997-2005 by Berend Engelbrecht. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the names QRes, Software Cave nor the names of any contributors to the software may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. BSD License template Copyright (c) 2005 by the Open Source Initiative http://www.opensource.org/licenses/bsd-license.php ================================================ FILE: DXMainClient/Resources/DTA/ts-ddraw-gdi.ini ================================================ ; ts-ddraw - https://github.com/CnCNet/ts-ddraw ; use the following settings to enable the experimental stretching support ; works only fullscreen right now + menus are not centered [ddraw] ; stretch to custom resolution, 0 = defaults to the size game requests StretchToWidth=0 StretchToHeight=0 ; override StretchToWidth/StretchToHeight and always stretch to fullscreen StretchToFullscreen=No ; use windowboxing to make a best fit Windowboxing=No ; maintain aspect ratio - this setting is ignored when Windowboxing is enabled MaintainAspectRatio=No ; 0 = never draw FPS, 1 = always draw FPS, 2 = draw FPS only when dropping frames DrawFPS=0 ; Enable vertical sync VSync=No ; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI Renderer=gdi ================================================ FILE: DXMainClient/Resources/DTA/ts-ddraw.ini ================================================ ; ts-ddraw - https://github.com/CnCNet/ts-ddraw ; use the following settings to enable the experimental stretching support ; works only fullscreen right now + menus are not centered [ddraw] ; stretch to custom resolution, 0 = defaults to the size game requests StretchToWidth=0 StretchToHeight=0 ; override StretchToWidth/StretchToHeight and always stretch to fullscreen StretchToFullscreen=No ; use windowboxing to make a best fit Windowboxing=No ; maintain aspect ratio - this setting is ignored when Windowboxing is enabled MaintainAspectRatio=No ; 0 = never draw FPS, 1 = always draw FPS, 2 = draw FPS only when dropping frames DrawFPS=0 ; Enable vertical sync VSync=No ; Select the renderer, opengl, gdi, auto. Default = auto = if OpenGL fails automatically use GDI Renderer=auto ================================================ FILE: DXMainClient/Resources/INI/Base/Instructions.txt ================================================ Any .ini file in the "Base" folder will be processed and copied to the "INI" folder every time a map is loaded. This makes it possible to make use of the "BaseSection=" key for any .ini file in this "Base" folder, which will make a section use all of the keys from the section it's referring to. This for example allows you to shorten the entire code of CIV1, CIV2, CIV3, CIV4, CIV5, CIV6 and CTECH from Rules.ini to the code shown below: [CIV1] Name=Civilian Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 Fraidycat=yes Civilian=yes Nominal=yes Pip=white VoiceSelect=67-N100,67-N102 VoiceMove=67-N104,67-N106,67-N108 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ImmuneToVeins=yes EliteAbilities=SCATTER [CIV2] BaseSection=CIV1 CrushSound=SQUISHY2 VoiceSelect=68-N100,68-N102,68-N104 VoiceMove=68-N106,68-N108,68-N110 [CIV3] BaseSection=CIV1 VoiceSelect=69-N100,69-N102,69-N104 VoiceMove=69-N106,69-N108,69-N110 [CIV4] BaseSection=CIV1 Image=CIV1 Fraidycat=no [CIV5] BaseSection=CIV2 Image=CIV2 Fraidycat=no [CIV6] BaseSection=CIV3 Image=CIV3 Fraidycat=no [CTECH] BaseSection=CIV6 Name=Technician Primary=Pistola Ammo=10 Reload=80 VoiceSelect=70-N000,70-N002,70-N004 VoiceMove=70-N006,70-N008,70-N010 VoiceAttack=70-N014,70-N016,70-N018 ================================================ FILE: DXMainClient/Resources/INI/Battle.ini ================================================ ;============================================================================ ; BATTLE.INI (English) ; ; This control file specifies the battle campaigns that ; are in the game. Additional files that begin with "BATTLE" ; will augment this list. ; ; $Author: Westwood Studios / Rampastring / E1 Elite$ ; $Archive: $ ; $Modtime: 31. 8. 2017$ ; $Revision:$ ;============================================================================ ; ******* Battle List ******* ; Lists the various battles in this control file. Each ; battle is given a unique (internal only) identifier name. [Battles] 0=TSCMPGNS 1=GDI1 2=NOD1 3=EMPTY 4=FSCMPGNS 5=GDIFS 6=NODFS 7=EMPTY 8=TUTORIALS 9=TSDEMO1 10=TSDEMO2 ;11=EMPTY ;12=EXTRAS ;13=GDI1DSHP ; ******* Individual Campaign Data ******* ; Each battle campaign lists its information ; in a section that cooresponds to its ; identifier battle name (see above). ; CD = the CD that must be present to play the campaign [-1 means any CD] ; Scenario = the scenario name for the first mission ; FinalMovie = finale movie to play at end of campaign (def=none) ; Description = text description of campaign for player choice list ; LongDescription = description of campaign for player choice list ; Side = the ID of the player's side (0 = GDI, 1 = Nod) ; SideName = determines which texture (side icon) to use for the list entry [TSCMPGNS] Description=- Tiberian Sun Campaigns - [GDI1] CD=0 Scenario=Maps\Missions\GDI1A.MAP IntroMovie=INTRO FinalMovie= Description=GDI - Evolutionary Response LongDescription=The official GDI Campaign of C&C: Tiberian Sun. Side=0 SideName=GDI [NOD1] CD=1 Scenario=Maps\Missions\NOD1A.MAP IntroMovie=INTRON FinalMovie= Description=Brotherhood of Nod - Deus ex Kane LongDescription=The official Nod Campaign of C&C: Tiberian Sun. Side=1 SideName=Nod [FSCMPGNS] Description=- Firestorm Campaigns - [GDIFS] Description=GDI - Desperate Measures Scenario=Maps\Missions\FSGDI01.MAP IntroMovie=FSGDIINT CD=2 RequiredAddon=1 Side=0 LongDescription=The official GDI Campaign of C&C: TS - Firestorm. SideName=GDI [NODFS] Description=Brotherhood of Nod - From the Ashes Scenario=Maps\Missions\FSNOD01.MAP IntroMovie=FSNODM01 CD=2 RequiredAddon=1 Side=1 LongDescription=The official Nod Campaign of C&C: TS - Firestorm. SideName=Nod [TUTORIALS] Description=- Tutorial Missions - [TSDEMO1] CD=-1 Scenario=Maps\Missions\TSDEMO1.MAP FinalMovie= Description=Tutorial 1 LongDescription=TS Tutorial #1 : Initiation Side=0 SideName=GDI [TSDEMO2] CD=-1 Scenario=Maps\Missions\TSDEMO2.MAP FinalMovie= Description=Tutorial 2 LongDescription=TS Tutorial #2 : Clean Sweep Side=0 SideName=GDI ; Adds an empty line to the list [EMPTY] Description= [EXTRAS] Description=- Extras - [GDI1DSHP] CD=0 Scenario=Maps\Missions\GDI1ADSHP.MAP FinalMovie= Description=Unfinished dropship loadout feature LongDescription=GDI 1A - Reinforce Phoenix Base Side=0 SideName=GDI ================================================ FILE: DXMainClient/Resources/INI/Default.ini ================================================ [AI] Paranoid=no ================================================ FILE: DXMainClient/Resources/INI/FSR.ini ================================================ [Houses] 00=GDI 01=Nod 02=Neutral 03=Special 04=Spawn1 05=Spawn2 06=Spawn3 07=Spawn4 08=Spawn5 09=Spawn6 10=Spawn7 11=Spawn8 [Spawn1] Color=Yellow [Spawn2] Color=HyundaiPurple [Spawn3] Color=Orange [Spawn4] Color=DarkOrange [Spawn5] Color=Magenta [Spawn6] Color=Blue [Spawn7] Color=LightCyan [Spawn8] Color=NeonGreen [BTIB01] Image=BTIB [BTIB02] Image=BTIB [BTIB03] Image=BTIB [BTIB04] Image=BTIB [BTIB05] Image=BTIB [BTIB06] Image=BTIB [BTIB07] Image=BTIB [BTIB08] Image=BTIB [BTIB09] Image=BTIB [BTIB10] Image=BTIB [BTIB11] Image=BTIB [BTIB12] Image=BTIB [LOBRDG01] Name=Low Bridge 01 [LOBRDG02] Name=Low Bridge 02 [LOBRDG03] Name=Low Bridge 03 [LOBRDG04] Name=Low Bridge 04 [LOBRDG05] Name=Low Bridge 05 (Damaged) [LOBRDG06] Name=Low Bridge 06 (Damaged) [LOBRDG07] Name=Low Bridge 07 (Damaged) [LOBRDG08] Name=Low Bridge 08 (Damaged) [LOBRDG09] Name=Low Bridge 09 (Damaged) [LOBRDG10] Name=Low Bridge 10 [LOBRDG11] Name=Low Bridge 11 [LOBRDG12] Name=Low Bridge 12 [LOBRDG13] Name=Low Bridge 13 [LOBRDG14] Name=Low Bridge 14 (Damaged) [LOBRDG15] Name=Low Bridge 15 (Damaged) [LOBRDG16] Name=Low Bridge 16 (Damaged) [LOBRDG17] Name=Low Bridge 17 (Damaged) [LOBRDG18] Name=Low Bridge 18 (Damaged) [LOBRDG19] Name=Low Bridge 19 (End) [LOBRDG20] Name=Low Bridge 20 (Damaged End) [LOBRDG21] Name=Low Bridge 21 (End) [LOBRDG22] Name=Low Bridge 22 (Damaged End) [LOBRDG23] Name=Low Bridge 23 (End) [LOBRDG24] Name=Low Bridge 24 (Damaged End) [LOBRDG25] Name=Low Bridge 25 (End) [LOBRDG26] Name=Low Bridge 26 (Damaged End) [LOBRDG27] Name=Low Bridge 27 (Destroyed) [LOBRDG28] Name=Low Bridge 28 (Destroyed) [LOBRDGE1] Name=Low Bridge End 1 [LOBRDGE2] Name=Low Bridge End 2 [LOBRDGE3] Name=Low Bridge End 3 [LOBRDGE4] Name=Low Bridge End 4 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Auto Deploy MCV.ini ================================================ [SUGMCV] ROT=100 [SUNMCV] ROT=100 [SUAMCV] ROT=100 [SUSMCV] ROT=100 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Disable Super Weapons.ini ================================================ [GAPLUG] TechLevel=-1 AIBuildThis=false [NAMISL] TechLevel=-1 AIBuildThis=false [NATMPL] SuperWeapon=none [NAWAST] TechLevel=-1 AIBuildThis=false ================================================ FILE: DXMainClient/Resources/INI/Game Options/Disable Unit Queueing.ini ================================================ [General] MaximumQueuedObjects=0 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Disable Visceroids.ini ================================================ [VISC_LRG] Strength=1 Primary=RangedSuicide GuardRange=512 TiberiumHeal=no Explosion=none [VISC_SML] Strength=1 Primary=RangedSuicide GuardRange=512 TiberiumHeal=no Explosion=none [RangedSuicide] Damage=0 ROF=10 Range=512 Projectile=Invisible Speed=100 Warhead=Super Report=DUMMY Anim=ELECTRO ================================================ FILE: DXMainClient/Resources/INI/Game Options/Extreme AI.ini ================================================ [General] TeamDelays=525,1175,1250 MultiplayerAICM=1100,380,200 MinimumAIDefensiveTeams=1,1,0 MaximumAIDefensiveTeams=2,2,0 TotalAITeamCap=16,14,12 [AI] BuildBarracks=GFACT_AI,NFACT_AI,AFACT_AI,SFACT_AI [AIHARV] Storage=42 [BuildingTypes] 1=GFACT_AI 2=NFACT_AI 3=AFACT_AI 4=SFACT_AI [GFACT] AIBuildThis=yes [NFACT] AIBuildThis=yes [AFACT] AIBuildThis=yes [SFACT] AIBuildThis=yes [GFACT_AI] Image=GFACT Nominal=yes Name=GDI Construction Yard ConstructionYard=yes Strength=1000 Armor=wood TechLevel=-1 Adjacent=1 Factory=BuildingType UndeploysInto=GMCV Sight=5 Owner=GDI Cost=1250 Points=50 Power=10 Capturable=true Crewed=yes Explosion=FBALL1 ScrapExplosion=FBALL1_SCRAP MaxDebris=0 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=700, 700, 500 AIBuildThis=yes TogglePower=no DeploySound=CONSTRU2 [NFACT_AI] Image=NFACT Nominal=yes Name=Nod Construction Yard ConstructionYard=yes Strength=1000 Armor=wood TechLevel=-1 Adjacent=1 Factory=BuildingType UndeploysInto=NMCV Sight=5 Owner=Nod Cost=1250 Points=50 Power=10 Capturable=true Crewed=yes Explosion=FBALL1 ScrapExplosion=FBALL1_SCRAP MaxDebris=0 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=700, 700, 500 AIBuildThis=yes TogglePower=no DeploySound=CONSTRU2 [AFACT_AI] Nominal=yes Image=RAFACT Name=Allied Construction Yard ConstructionYard=yes Strength=1000 Armor=heavy TechLevel=-1 Adjacent=1 Factory=BuildingType UndeploysInto=AMCV Sight=5 Owner=Allies Cost=1250 Points=25 Power=0 Capturable=true Crewed=yes Explosion=FBALL1 ScrapExplosion=FBALL1_SCRAP MaxDebris=0 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=700, 700, 500 AIBuildThis=yes TogglePower=no DeploySound=CONSTRU2 [SFACT_AI] Nominal=yes Image=RAFACT Name=Soviet Construction Yard ConstructionYard=yes Strength=1000 Armor=heavy TechLevel=-1 Adjacent=1 Factory=BuildingType UndeploysInto=SMCV Sight=5 Owner=Soviet Cost=1250 Points=25 Power=0 Capturable=true Crewed=yes Explosion=FBALL1 ScrapExplosion=FBALL1_SCRAP MaxDebris=0 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=700, 700, 500 AIBuildThis=yes TogglePower=no DeploySound=CONSTRU2 [RAPOWR] AIBuildThis=yes ================================================ FILE: DXMainClient/Resources/INI/Game Options/Harder AI.ini ================================================ [General] MultiplayerAICM=250,200,100 HealScanRadius=10 FillEarliestTeamProbability=100,80,60 MinimumAIDefensiveTeams=0,0,0 MaximumAIDefensiveTeams=1,0,0 TotalAITeamCap=14,12,10 TeamDelays=500,1800,2200 AIHateDelays=400,1500,3500 [Easy] Groundspeed=1.0 Airspeed=1.0 BuildTime=.8 Armor=1.2 ROF=.8 Cost=0.5 RepairDelay=.02 BuildDelay=.03 DestroyWalls=yes ContentScan=yes [Difficult] Groundspeed=1.0 Airspeed=1.0 BuildTime=2.0 Armor=.8 ROF=1.2 Cost=0.5 RepairDelay=.05 BuildDelay=.1 BuildSlowdown=yes DestroyWalls=no ================================================ FILE: DXMainClient/Resources/INI/Game Options/Immune Harvesters.ini ================================================ [HARV] Immune=yes Crusher=no LegalTarget=no Insignificant=yes Sight=1 MovementZone=Normal [HORV] Immune=yes Crusher=no LegalTarget=no Insignificant=yes ================================================ FILE: DXMainClient/Resources/INI/Game Options/Infinite Tiberium.ini ================================================ [General] HarvesterLoadRate=20.0 HarvesterDumpRate=0.16 [HARV] Storage=3 [TIBTRE01] AnimationRate=1 AnimationProbability=1 [TIBTRE02] AnimationRate=1 AnimationProbability=1 [TIBTRE03] AnimationRate=1 AnimationProbability=1 [Tiberiums] 0=Riparius 1=Cruentus 2=Vinifera 3=Aboreus [Riparius] Name=Tiberium Riparius Image=1 Power=4 Value=234 Growth=2200 GrowthPercentage=.09 Spread=2200 SpreadPercentage=.09 Color=NeonGreen [Cruentus] Name=Tiberium Cruentus Image=2 Value=700 Growth=10000 GrowthPercentage=0 Spread=10000 SpreadPercentage=0 Power=10 Color=NeonBlue Debris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4 [Vinifera] Name=Tiberium Vinifera Image=3 Value=467 Growth=10000 GrowthPercentage=.05 Spread=10000 SpreadPercentage=.05 Power=100 ; 10 Color=NeonBlue Debris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4 [Aboreus] Name=Tiberium Aboreus Image=4 Value=300 Growth=10000 GrowthPercentage=.05 Spread=10000 SpreadPercentage=.05 Power=10 Color=NeonBlue Debris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Ingame Allying.ini ================================================ [Tutorial] 925=Ingame allying is allowed. To ally with another human player, select any unit or structure of the player you want to form an alliance with and press "A" on your keyboard. [Tags] IngameAllyingTag=0,Ingame Allying 1,IngameAllying [Triggers] IngameAllying=Neutral,,Ingame Allying,0,1,1,1,0 [Events] IngameAllying=1,8,0,0 [Actions] IngameAllying=1,11,0,925,0,0,0,0,A ================================================ FILE: DXMainClient/Resources/INI/Game Options/Instant Harvester Unload.ini ================================================ [General] HarvesterDumpRate=0 [PROC] Image=PROCI [TDPROC] Image=TDPROCI [RAPROC] Image=RAPROCI [PROC_AI] Image=PROCI [TDPROC_AI] Image=TDPROCI [RAPROC_AI] Image=RAPROCI ================================================ FILE: DXMainClient/Resources/INI/Game Options/Naval.ini ================================================ [GSYRD] TechLevel=5 [NSYRD] TechLevel=5 [ASYRD] TechLevel=5 [RASPEN] TechLevel=5 [GSYRD_AI] TechLevel=5 [NSYRD_AI] TechLevel=5 [ASYRD_AI] TechLevel=5 [RASPEN_AI] TechLevel=5 [WEAP_AI] WeaponsFactory=no [AFLD_AI] WeaponsFactory=no [AWEAP_AI] WeaponsFactory=no [SWEAP_AI] WeaponsFactory=no ================================================ FILE: DXMainClient/Resources/INI/Game Options/No Baddy Crates.ini ================================================ [Powerups] Armor=33,ARMOR,0.5 Cloak=20,CLOAK Darkness=0,SHROUDX Explosion=0,RAPID,0 Firepower=28,FIREPOWR,2.0 HealBase=23,HEALALL ICBM=10,CHEMISLE Money=35,MONEY,2000 Napalm=0,,0 Reveal=0,REVEAL Speed=30,SPEED,1.7 Squad=0, Unit=70, Invulnerability=15,AREAHEAL,1.0 Veteran=30,VETERAN,1 IonStorm=0, Gas=0,,0 Tiberium=25, Pod=0, ================================================ FILE: DXMainClient/Resources/INI/Game Options/No Crew.ini ================================================ [General] CrewEscape=0% [GFACT] Crewed=no [NFACT] Crewed=no [NUKE] Crewed=no [NUK2] Crewed=no [PROC] Crewed=no [PYLE] Crewed=no [HAND] Crewed=no [TWR] Crewed=no [GUN] Crewed=no [RAGUN] Crewed=no [SAM] Crewed=no [ATWR] Crewed=no [OBLI] Crewed=no [DTERM] Crewed=no [HQ] Crewed=no [NHQ] Crewed=no [GHPAD] Crewed=no [NHPAD] Crewed=no [AHPAD] Crewed=no [ASTRP] Crewed=no [RAASTRP] Crewed=no [WEAP] Crewed=no [AFLD] Crewed=no [FIX] Crewed=no [EYE] Crewed=no [COMM] Crewed=no [SGEN] Crewed=no [TMPL] Crewed=no [MISS] Crewed=no [AFACT] Crewed=no [SFACT] Crewed=no [RAPOWR] Crewed=no [RAAPWR] Crewed=no [RATENT] Crewed=no [RABARR] Crewed=no [RAKENN] Crewed=no [RAPROC] Crewed=no [RATSLA] Crewed=no [RADOME] Crewed=no [AWEAP] Crewed=no [SWEAP] Crewed=no [RAFCOM] Crewed=no [RAFIX] Crewed=no [RAATEK] Crewed=no [RASTEK] Crewed=no [RAIRON] Crewed=no [NAMSLO] Crewed=no [AMSLO] Crewed=no [RAPDOX] Crewed=no [NUKE_AI] Crewed=no [NUK2_AI] Crewed=no [NUK2A_AI] Crewed=no [NUK2B_AI] Crewed=no [NUK2C_AI] Crewed=no [NUK2D_AI] Crewed=no [NUK2E_AI] Crewed=no [NUK2G_AI] Crewed=no [RAPOWR_AI] Crewed=no [RAAPWR_AI] Crewed=no [PROC_AI] Crewed=no [PYLE_AI] Crewed=no [HAND_AI] Crewed=no [RATENT_AI] Crewed=no [RABARR_AI] Crewed=no [TWR_AI] Crewed=no [OBLI_AI] Crewed=no [RAGUN_AI] Crewed=no [RATSLA_AI] Crewed=no [HPAD_AI] Crewed=no [AHPAD_AI] Crewed=no [WEAP_AI] Crewed=no [AFLD_AI] Crewed=no [AWEAP_AI] Crewed=no [SWEAP_AI] Crewed=no [EYE_AI] Crewed=no [RAATEK_AI] Crewed=no [RASTEK_AI] Crewed=no [RAMSLO_AI] Crewed=no ================================================ FILE: DXMainClient/Resources/INI/Game Options/No Silos.ini ================================================ [Units] K=Spawn1,STORAGEU,256,0,2,64,Unload,None,0,-1,0,-1,1,0 L=Spawn2,STORAGEU,256,1,2,64,Unload,None,0,-1,0,-1,1,0 M=Spawn3,STORAGEU,256,2,2,64,Unload,None,0,-1,0,-1,1,0 N=Spawn4,STORAGEU,256,3,2,64,Unload,None,0,-1,0,-1,1,0 O=Spawn5,STORAGEU,256,4,2,64,Unload,None,0,-1,0,-1,1,0 P=Spawn6,STORAGEU,256,5,2,64,Unload,None,0,-1,0,-1,1,0 Q=Spawn7,STORAGEU,256,6,2,64,Unload,None,0,-1,0,-1,1,0 R=Spawn8,STORAGEU,256,7,2,64,Unload,None,0,-1,0,-1,1,0 [PROC] Storage=0 PipScale=none [SILO] TechLevel=-1 [VehicleTypes] 1=STORAGEU [STORAGEU] Nominal=yes ROT=16 Strength=1 Selectable=false Insignificant=yes Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} DeploysInto=STORAGE Secondary=RangedSuicide DeployToFire=yes GuardRange=512 [RangedSuicide] Damage=0 ROF=10 Range=512 Projectile=Invisible Speed=100 Warhead=Super [BuildingTypes] 1=STORAGE [STORAGE] Storage=99999999 Image=GALITE Strength=600 Armor=wood TogglePower=no Selectable=no Immune=yes LegalTarget=no Insignificant=yes BridgeRepairHut=yes PlaceAnywhere=yes InvisibleInGame=yes BaseNormal=no ================================================ FILE: DXMainClient/Resources/INI/Game Options/Replace Tiberium With Ore.ini ================================================ [Tiberiums] 0=Riparius 1=Cruentus 2=Vinifera 3=Aboreus [Riparius] Name=Ore Color=DarkGold Image=1 Power=0 Value=25 Growth=1000 Spread=1000 GrowthPercentage=10 SpreadPercentage=2 [Vinifera] Name=Gems Color=Red Image=3 Power=0 Value=40 Growth=1000 GrowthPercentage=10 Spread=10000 SpreadPercentage=0 [Cruentus] Name=Gems Color=Red Image=2 Power=0 Value=40 Growth=1000 GrowthPercentage=10 Spread=10000 SpreadPercentage=0 [TIBTREE] Image=OREMINE2 Name=Ore Mine [TIBTREE2] Image=OREMINE2 Name=Ore Mine [TIBTREE3] Image=OREMINE2 Name=Ore Mine [VINTREE] Image=OREMINE3 Name=Gem Mine [VINTREE2] Image=OREMINE3 Name=Gem Mine [VINTREE3] Image=OREMINE3 Name=Gem Mine [OREMINE3] Image=OREMINE2 [RTIB01] Image=ORE01 ChainReaction=no [RTIB02] Image=ORE02 ChainReaction=no [RTIB03] Image=ORE03 ChainReaction=no [RTIB04] Image=ORE04 ChainReaction=no [RTIB05] Image=ORE01 ChainReaction=no [RTIB06] Image=ORE02 ChainReaction=no [RTIB07] Image=ORE03 ChainReaction=no [RTIB08] Image=ORE04 ChainReaction=no [RTIB09] Image=ORE01 ChainReaction=no [RTIB10] Image=ORE02 ChainReaction=no [RTIB11] Image=ORE03 ChainReaction=no [RTIB12] Image=ORE04 ChainReaction=no [QTIB01] Image=GEMRA01 ChainReaction=no [QTIB02] Image=GEMRA02 ChainReaction=no [QTIB03] Image=GEMRA03 ChainReaction=no [QTIB04] Image=GEMRA04 ChainReaction=no [QTIB05] Image=GEMRA01 ChainReaction=no [QTIB06] Image=GEMRA02 ChainReaction=no [QTIB07] Image=GEMRA03 ChainReaction=no [QTIB08] Image=GEMRA04 ChainReaction=no [QTIB09] Image=GEMRA01 ChainReaction=no [QTIB10] Image=GEMRA02 ChainReaction=no [QTIB11] Image=GEMRA03 ChainReaction=no [QTIB12] Image=GEMRA04 ChainReaction=no ================================================ FILE: DXMainClient/Resources/INI/Game Options/Reveal Shroud.ini ================================================ [Events] RevealOption=1,8,0,0 [Actions] RevealOption=1,16,0,0,0,0,0,0,A [Tags] RevealOptionTag=0,Reveal 1,RevealOption [Triggers] RevealOption=Neutral,,Reveal,0,1,1,1,0 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Shroud Regrows.ini ================================================ [AudioVisual] ShroudGrow=yes ================================================ FILE: DXMainClient/Resources/INI/Game Options/Starting Units.ini ================================================ [StartingUnitsScript] 0=21,11 Name=Scatter [1TNK-SUTaskForce] 0=1,1TNK Name=Starting 1TNK Group=-1 [2TNK-SUTaskForce] 0=1,2TNK Name=Starting 2TNK Group=-1 [3TNK-SUTaskForce] 0=1,3TNK Name=Starting 3TNK Group=-1 [Actions] StartingUnits1=8,80,1,Spawn1-MTNK,0,0,0,0,A,80,1,Spawn1-HVCT,0,0,0,0,A,80,1,Spawn1-1TNK,0,0,0,0,A,80,1,Spawn1-2TNK,0,0,0,0,A,80,1,Spawn1-3TNK,0,0,0,0,A,80,1,Spawn1-Infantry,0,0,0,0,A,80,1,Spawn1-Infantry,0,0,0,0,A,80,1,Spawn1-Infantry,0,0,0,0,A StartingUnits2=8,80,1,Spawn2-MTNK,0,0,0,0,B,80,1,Spawn2-HVCT,0,0,0,0,B,80,1,Spawn2-1TNK,0,0,0,0,B,80,1,Spawn2-2TNK,0,0,0,0,B,80,1,Spawn2-3TNK,0,0,0,0,B,80,1,Spawn2-Infantry,0,0,0,0,B,80,1,Spawn2-Infantry,0,0,0,0,B,80,1,Spawn2-Infantry,0,0,0,0,B StartingUnits3=8,80,1,Spawn3-MTNK,0,0,0,0,C,80,1,Spawn3-HVCT,0,0,0,0,C,80,1,Spawn3-1TNK,0,0,0,0,C,80,1,Spawn3-2TNK,0,0,0,0,C,80,1,Spawn3-3TNK,0,0,0,0,C,80,1,Spawn3-Infantry,0,0,0,0,C,80,1,Spawn3-Infantry,0,0,0,0,C,80,1,Spawn3-Infantry,0,0,0,0,C StartingUnits4=8,80,1,Spawn4-MTNK,0,0,0,0,D,80,1,Spawn4-HVCT,0,0,0,0,D,80,1,Spawn4-1TNK,0,0,0,0,D,80,1,Spawn4-2TNK,0,0,0,0,D,80,1,Spawn4-3TNK,0,0,0,0,D,80,1,Spawn4-Infantry,0,0,0,0,D,80,1,Spawn4-Infantry,0,0,0,0,D,80,1,Spawn4-Infantry,0,0,0,0,D StartingUnits5=8,80,1,Spawn5-MTNK,0,0,0,0,E,80,1,Spawn5-HVCT,0,0,0,0,E,80,1,Spawn5-1TNK,0,0,0,0,E,80,1,Spawn5-2TNK,0,0,0,0,E,80,1,Spawn5-3TNK,0,0,0,0,E,80,1,Spawn5-Infantry,0,0,0,0,E,80,1,Spawn5-Infantry,0,0,0,0,E,80,1,Spawn5-Infantry,0,0,0,0,E StartingUnits6=8,80,1,Spawn6-MTNK,0,0,0,0,F,80,1,Spawn6-HVCT,0,0,0,0,F,80,1,Spawn6-1TNK,0,0,0,0,F,80,1,Spawn6-2TNK,0,0,0,0,F,80,1,Spawn6-3TNK,0,0,0,0,F,80,1,Spawn6-Infantry,0,0,0,0,F,80,1,Spawn6-Infantry,0,0,0,0,F,80,1,Spawn6-Infantry,0,0,0,0,F StartingUnits7=8,80,1,Spawn7-MTNK,0,0,0,0,G,80,1,Spawn7-HVCT,0,0,0,0,G,80,1,Spawn7-1TNK,0,0,0,0,G,80,1,Spawn7-2TNK,0,0,0,0,G,80,1,Spawn7-3TNK,0,0,0,0,G,80,1,Spawn7-Infantry,0,0,0,0,G,80,1,Spawn7-Infantry,0,0,0,0,G,80,1,Spawn7-Infantry,0,0,0,0,G StartingUnits8=8,80,1,Spawn8-MTNK,0,0,0,0,H,80,1,Spawn8-HVCT,0,0,0,0,H,80,1,Spawn8-1TNK,0,0,0,0,H,80,1,Spawn8-2TNK,0,0,0,0,H,80,1,Spawn8-3TNK,0,0,0,0,H,80,1,Spawn8-Infantry,0,0,0,0,H,80,1,Spawn8-Infantry,0,0,0,0,H,80,1,Spawn8-Infantry,0,0,0,0,H [Events] StartingUnits1=1,13,0,50 StartingUnits2=1,13,0,50 StartingUnits3=1,13,0,50 StartingUnits4=1,13,0,50 StartingUnits5=1,13,0,50 StartingUnits6=1,13,0,50 StartingUnits7=1,13,0,50 StartingUnits8=1,13,0,50 [Infantry-SUTaskForce] 0=1,E2 1=1,E4 2=1,E1 Name=Starting Infantry Group=-1 [HVCT-SUTaskForce] 0=1,HVCT Name=Starting HVCT Group=-1 [MTNK-SUTaskForce] 0=1,MTNK Name=Starting MTNK Group=-1 [ScriptTypes] SU=StartingUnitsScript [Spawn1-1TNK] Max=5 Full=yes Name=Spawn1 Starting 1TNK Group=-1 House=Spawn1 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=A Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn1-2TNK] Max=5 Full=yes Name=Spawn1 Starting 2TNK Group=-1 House=Spawn1 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=A Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn1-3TNK] Max=5 Full=yes Name=Spawn1 Starting 3TNK Group=-1 House=Spawn1 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=A Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn1-Infantry] Max=5 Full=yes Name=Spawn1 Starting Infantry Group=-1 House=Spawn1 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=A Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn1-HVCT] Max=5 Full=yes Name=Spawn1 Starting HVCT Group=-1 House=Spawn1 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=A Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn1-MTNK] Max=5 Full=yes Name=Spawn1 Starting MTNK Group=-1 House=Spawn1 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=A Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn2-1TNK] Max=5 Full=yes Name=Spawn2 Starting 1TNK Group=-1 House=Spawn2 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=B Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn2-2TNK] Max=5 Full=yes Name=Spawn2 Starting 2TNK Group=-1 House=Spawn2 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=B Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn2-3TNK] Max=5 Full=yes Name=Spawn2 Starting 3TNK Group=-1 House=Spawn2 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=B Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn2-Infantry] Max=5 Full=yes Name=Spawn2 Starting Infantry Group=-1 House=Spawn2 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=B Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn2-HVCT] Max=5 Full=yes Name=Spawn2 Starting HVCT Group=-1 House=Spawn2 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=B Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn2-MTNK] Max=5 Full=yes Name=Spawn2 Starting MTNK Group=-1 House=Spawn2 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=B Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn3-1TNK] Max=5 Full=yes Name=Spawn3 Starting 1TNK Group=-1 House=Spawn3 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=C Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn3-2TNK] Max=5 Full=yes Name=Spawn3 Starting 2TNK Group=-1 House=Spawn3 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=C Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn3-3TNK] Max=5 Full=yes Name=Spawn3 Starting 3TNK Group=-1 House=Spawn3 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=C Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn3-Infantry] Max=5 Full=yes Name=Spawn3 Starting Infantry Group=-1 House=Spawn3 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=C Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn3-HVCT] Max=5 Full=yes Name=Spawn3 Starting HVCT Group=-1 House=Spawn3 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=C Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn3-MTNK] Max=5 Full=yes Name=Spawn3 Starting MTNK Group=-1 House=Spawn3 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=C Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn4-1TNK] Max=5 Full=yes Name=Spawn4 Starting 1TNK Group=-1 House=Spawn4 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=D Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn4-2TNK] Max=5 Full=yes Name=Spawn4 Starting 2TNK Group=-1 House=Spawn4 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=D Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn4-3TNK] Max=5 Full=yes Name=Spawn4 Starting 3TNK Group=-1 House=Spawn4 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=D Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn4-Infantry] Max=5 Full=yes Name=Spawn4 Starting Infantry Group=-1 House=Spawn4 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=D Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn4-HVCT] Max=5 Full=yes Name=Spawn4 Starting HVCT Group=-1 House=Spawn4 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=D Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn4-MTNK] Max=5 Full=yes Name=Spawn4 Starting MTNK Group=-1 House=Spawn4 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=D Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn5-1TNK] Max=5 Full=yes Name=Spawn5 Starting 1TNK Group=-1 House=Spawn5 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=E Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn5-2TNK] Max=5 Full=yes Name=Spawn5 Starting 2TNK Group=-1 House=Spawn5 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=E Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn5-3TNK] Max=5 Full=yes Name=Spawn5 Starting 3TNK Group=-1 House=Spawn5 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=E Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn5-Infantry] Max=5 Full=yes Name=Spawn5 Starting Infantry Group=-1 House=Spawn5 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=E Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn5-HVCT] Max=5 Full=yes Name=Spawn5 Starting HVCT Group=-1 House=Spawn5 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=E Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn5-MTNK] Max=5 Full=yes Name=Spawn5 Starting MTNK Group=-1 House=Spawn5 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=E Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn6-1TNK] Max=5 Full=yes Name=Spawn6 Starting 1TNK Group=-1 House=Spawn6 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=F Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn6-2TNK] Max=5 Full=yes Name=Spawn6 Starting 2TNK Group=-1 House=Spawn6 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=F Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn6-3TNK] Max=5 Full=yes Name=Spawn6 Starting 3TNK Group=-1 House=Spawn6 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=F Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn6-Infantry] Max=5 Full=yes Name=Spawn6 Starting Infantry Group=-1 House=Spawn6 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=F Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn6-HVCT] Max=5 Full=yes Name=Spawn6 Starting HVCT Group=-1 House=Spawn6 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=F Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn6-MTNK] Max=5 Full=yes Name=Spawn6 Starting MTNK Group=-1 House=Spawn6 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=F Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn7-1TNK] Max=5 Full=yes Name=Spawn7 Starting 1TNK Group=-1 House=Spawn7 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=G Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn7-2TNK] Max=5 Full=yes Name=Spawn7 Starting 2TNK Group=-1 House=Spawn7 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=G Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn7-3TNK] Max=5 Full=yes Name=Spawn7 Starting 3TNK Group=-1 House=Spawn7 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=G Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn7-Infantry] Max=5 Full=yes Name=Spawn7 Starting Infantry Group=-1 House=Spawn7 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=G Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn7-HVCT] Max=5 Full=yes Name=Spawn7 Starting HVCT Group=-1 House=Spawn7 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=G Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn7-MTNK] Max=5 Full=yes Name=Spawn7 Starting MTNK Group=-1 House=Spawn7 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=G Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn8-1TNK] Max=5 Full=yes Name=Spawn8 Starting 1TNK Group=-1 House=Spawn8 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=H Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=1TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn8-2TNK] Max=5 Full=yes Name=Spawn8 Starting 2TNK Group=-1 House=Spawn8 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=H Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=2TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn8-3TNK] Max=5 Full=yes Name=Spawn8 Starting 3TNK Group=-1 House=Spawn8 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=H Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=3TNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn8-Infantry] Max=5 Full=yes Name=Spawn8 Starting Infantry Group=-1 House=Spawn8 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=H Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=Infantry-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn8-HVCT] Max=5 Full=yes Name=Spawn8 Starting HVCT Group=-1 House=Spawn8 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=H Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=HVCT-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Spawn8-MTNK] Max=5 Full=yes Name=Spawn8 Starting MTNK Group=-1 House=Spawn8 Script=StartingUnitsScript Whiner=no Droppod=no Suicide=no Loadable=no Prebuild=no Priority=5 Waypoint=H Annoyance=no IonImmune=no Recruiter=no Reinforce=no TaskForce=MTNK-SUTaskForce TechLevel=0 Aggressive=no Autocreate=yes GuardSlower=no OnTransOnly=no AvoidThreats=no LooseRecruit=no VeteranLevel=1 IsBaseDefense=no OnlyTargetHouseEnemy=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=no [Tags] StartingUnits1Tag=0,Spawn1 Starting Units 1,StartingUnits1 StartingUnits2Tag=0,Spawn2 Starting Units 1,StartingUnits2 StartingUnits3Tag=0,Spawn3 Starting Units 1,StartingUnits3 StartingUnits4Tag=0,Spawn4 Starting Units 1,StartingUnits4 StartingUnits5Tag=0,Spawn5 Starting Units 1,StartingUnits5 StartingUnits6Tag=0,Spawn6 Starting Units 1,StartingUnits6 StartingUnits7Tag=0,Spawn7 Starting Units 1,StartingUnits7 StartingUnits8Tag=0,Spawn8 Starting Units 1,StartingUnits8 [TaskForces] SU0=MTNK-SUTaskForce SU1=HVCT-SUTaskForce SU2=1TNK-SUTaskForce SU3=2TNK-SUTaskForce SU4=3TNK-SUTaskForce SU5=Infantry-SUTaskForce [TeamTypes] SU00=Spawn1-MTNK SU01=Spawn2-MTNK SU02=Spawn3-MTNK SU03=Spawn4-MTNK SU04=Spawn5-MTNK SU05=Spawn6-MTNK SU06=Spawn7-MTNK SU07=Spawn8-MTNK SU08=Spawn1-HVCT SU09=Spawn1-1TNK SU10=Spawn1-2TNK SU11=Spawn1-3TNK SU12=Spawn1-Infantry SU13=Spawn4-HVCT SU14=Spawn2-HVCT SU15=Spawn2-1TNK SU16=Spawn2-2TNK SU17=Spawn2-3TNK SU18=Spawn2-Infantry SU19=Spawn4-1TNK SU20=Spawn3-HVCT SU21=Spawn3-1TNK SU22=Spawn3-2TNK SU23=Spawn3-3TNK SU24=Spawn3-Infantry SU25=Spawn4-2TNK SU26=Spawn4-3TNK SU27=Spawn4-Infantry SU28=Spawn5-HVCT SU29=Spawn5-1TNK SU30=Spawn5-2TNK SU31=Spawn5-3TNK SU32=Spawn5-Infantry SU33=Spawn6-HVCT SU34=Spawn6-1TNK SU35=Spawn6-2TNK SU36=Spawn6-3TNK SU37=Spawn6-Infantry SU38=Spawn7-HVCT SU39=Spawn7-1TNK SU40=Spawn7-2TNK SU41=Spawn7-3TNK SU42=Spawn7-Infantry SU43=Spawn8-HVCT SU44=Spawn8-1TNK SU45=Spawn8-2TNK SU46=Spawn8-3TNK SU47=Spawn8-Infantry [Triggers] StartingUnits1=Neutral,,Spawn1 Starting Units,0,1,1,1,0 StartingUnits2=Neutral,,Spawn2 Starting Units,0,1,1,1,0 StartingUnits3=Neutral,,Spawn3 Starting Units,0,1,1,1,0 StartingUnits4=Neutral,,Spawn4 Starting Units,0,1,1,1,0 StartingUnits5=Neutral,,Spawn5 Starting Units,0,1,1,1,0 StartingUnits6=Neutral,,Spawn6 Starting Units,0,1,1,1,0 StartingUnits7=Neutral,,Spawn7 Starting Units,0,1,1,1,0 StartingUnits8=Neutral,,Spawn8 Starting Units,0,1,1,1,0 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Storms.ini ================================================ [Actions] StormsOption=1,44,0,300,0,0,0,0,A [Events] StormsOption=1,51,0,1000 [Tags] StormsOptionTag=2,Ion Storm 1,StormsOption [Triggers] StormsOption=Neutral,,Ion Storm,0,1,1,1,0 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Turbo Vehicles.ini ================================================ [Clear] Track=69% Wheel=69% Hover=69% Amphibious=69% [Rough] Track=69% Wheel=69% Hover=69% Amphibious=69% [Road] Track=69% Wheel=69% Hover=69% Amphibious=69% [Water] Hover=69% Float=69% Amphibious=69% [Tiberium] Track=69% Wheel=69% Hover=69% Amphibious=69% [Weeds] Track=69% Wheel=69% Hover=69% Amphibious=69% [Beach] Hover=69% Amphibious=69% [Railroad] Track=69% Wheel=69% Hover=69% Amphibious=69% [Ice] Hover=69% Amphibious=69% [Tunnel] Track=69% Wheel=69% Hover=69% Amphibious=69% [LTNK] Speed=8 [1TNK] Speed=9 [MTNK] Speed=8 [2TNK] Speed=8 [FTNK] Speed=8 [3TNK] Speed=7 [SOVAPC] Speed=8 [MFLAK] Speed=8 [DTRK] Speed=8 [TERMITE] Speed=8 [MWAVE] Speed=7 [TTNK] Speed=8 [ARTY] Speed=6 [RAARTY] Speed=6 [MSAM] Speed=8 [MLRS] Speed=8 [V2RL] Speed=7 [XO] Speed=8 [SSM] Speed=8 [SAPC] Speed=7 [MRV] Speed=8 [MSA] Speed=8 [AIXO] Speed=8 [AIMTNK] Speed=8 [AILTNK] Speed=8 [AIFTNK] Speed=8 [AIMWAVE] Speed= [AIMLRS] Speed=8 [AIARTY] Speed=6 [AIMSAM] Speed=8 [AI2TNK] Speed=8 [AI3TNK] Speed=7 [AIV2RL] Speed=7 [AITTNK] Speed=8 [AIHVCT] Speed=8 [AIMFLAK] Speed=8 [HVCT] Speed=8 [HVR] Speed=10 [HVC] Speed=8 [HVCSAM] Speed=7 [LTNKCRUS] Speed=8 [MWAVEMSAM] Speed=7 [UTNK] Speed=7 [CUMRV] Speed=8 [CUMSA] Speed=8 [ANT1] Speed=8 [ANT2] Speed=8 [ANT3] Speed=8 [ANT4] Speed=8 [ANT5] Speed=8 ;[MHQ] ;Speed=8 ;6 ; ;[HARV] ;Speed=8 ;6 ; ;[GMCV] ;Speed=8 ;6 ; ;[NMCV] ;Speed=8 ;6 ; ;[AMCV] ;Speed=8 ;6 ; ;[SMCV] ;Speed=8 ;6 ; ;[SUGMCV] ;Speed=8 ;6 ; ;[SUNMCV] ;Speed=8 ;6 ; ;[SUAMCV] ;Speed=8 ;6 ; ;[SUSMCV] ;Speed=8 ;6 ; ;[FUHARV] ;Speed=8 ;6 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Turtling AI.ini ================================================ [General] AIHateDelays=99999999,99999999,99999999 [TaskForces] TurtlingAI01=08603140-G TurtlingAI02=07EA1E90-G TurtlingAI03=08602820-G TurtlingAI04=0860DE90-G TurtlingAI05=09F7B380-G TurtlingAI06=0860314M-G TurtlingAI07=09F7B38M-G TurtlingAI08=00GAIRE1-G TurtlingAI09=00NAIRE1-G TurtlingAI10=0860282M-G TurtlingAI11=0860DE9M-G TurtlingAI12=08601B60-A TurtlingAI13=00GAIRE1-A TurtlingAI14=0860314M-A TurtlingAI15=07EA1E9M-A TurtlingAI16=09F7B38M-A TurtlingAI17=08603140-A TurtlingAI18=07EA1E90-A TurtlingAI19=09F7B380-A TurtlingAI20=00GAIRE1-S TurtlingAI21=0860314M-S TurtlingAI22=07EA1E9M-S TurtlingAI23=09F7B38M-S TurtlingAI24=08603140-S TurtlingAI25=09F7B380-S TurtlingAI26=07EA1E90-S [08603140-G] 0=1,ORCA [07EA1E90-G] 0=1,ORCA [08602820-G] 0=1,APACHE [0860DE90-G] 0=1,APACHE [09F7B380-G] 0=1,ORCA [0860314M-G] 0=1,ORCA [09F7B38M-G] 0=1,ORCA [00GAIRE1-G] 0=1,ORCA [00NAIRE1-G] 0=1,APACHE [0860282M-G] 0=1,APACHE [0860DE9M-G] 0=1,APACHE [08601B60-A] 0=1,HELI [00GAIRE1-A] 0=1,HELI [0860314M-A] 0=1,HELI [07EA1E9M-A] 0=1,HELI [09F7B38M-A] 0=1,HELI [08603140-A] 0=1,HELI [07EA1E90-A] 0=1,HELI [09F7B380-A] 0=1,HELI [00GAIRE1-S] 0=1,YAK [0860314M-S] 1=1,MIG [07EA1E9M-S] 0=1,MIG [09F7B38M-S] 0=1,YAK [08603140-S] 1=1,MIG [09F7B380-S] 0=1,YAK [07EA1E90-S] 0=1,MIG [ScriptTypes] TurtlingAI0=085F3E00-G TurtlingAI1=085F3980-G TurtlingAI2=075A3070-G TurtlingAI3=07F7B2A0-G TurtlingAI4=07E686F0-G TurtlingAI5=07F7C5E0-G TurtlingAI6=0786DA60-G TurtlingAI7=07F7D0D0-G TurtlingAI8=07F7E3B0-G TurtlingAI9=0960AAA0-G TurtlingAI10=07F76BE0-G TurtlingAI11=075AD760-G TurtlingAI12=08462780-G TurtlingAI13=075ABE00-G TurtlingAI14=08463030-G TurtlingAI15=088DDE00-G TurtlingAI16=07397BE0-G TurtlingAI17=07F3DE00-G TurtlingAI18=08B50140-G TurtlingAI19=0H000000-G TurtlingAI20=0TR00000-G TurtlingAI21=0TR00001-G TurtlingAI22=0HUNT000-G TurtlingAI23=MCV_IDLE TurtlingAI24=MCV_DEPLOY TurtlingAI1019=GNAVALA0-G TurtlingAI1020=NNAVALA0-G TurtlingAI1021=SNAVALA0-G TurtlingAI1023=00NODCY0-G TurtlingAI1024=00SOVCY0-G TurtlingAI1025=00ALDCY0-G TurtlingAI1027=0HHH0000-G [085F3E00-G] 0=11,10 [085F3980-G] 0=11,10 [075A3070-G] 0=11,10 [07F7B2A0-G] 0=11,10 [07E686F0-G] 0=11,10 [07F7C5E0-G] 0=11,10 [0786DA60-G] 0=11,10 [07F7D0D0-G] 0=11,10 [07F7E3B0-G] 0=11,10 [0960AAA0-G] 0=11,10 [07F76BE0-G] 0=11,10 [075AD760-G] 0=11,10 [08462780-G] 0=11,10 [075ABE00-G] 0=11,10 [08463030-G] 0=11,10 [088DDE00-G] 0=11,10 [07F3DE00-G] 0=11,10 [08B50140-G] 0=9,0 [07397BE0-G] 0=11,10 [0H000000-G] 0=11,10 [0TR00000-G] 0=11,10 [0TR00001-G] 0=11,10 [0HUNT000-G] 0=11,10 [GNAVALA0-G] 0=11,10 [NNAVALA0-G] 0=11,10 [SNAVALA0-G] 0=11,10 [00NODCY0-G] 0=11,10 [00SOVCY0-G] 0=11,10 [00ALDCY0-G] 0=11,10 [0HHH0000-G] 0=11,10 ================================================ FILE: DXMainClient/Resources/INI/Game Options/Uncrushable Infantry.ini ================================================ [E1] Crushable=no [E1N] Crushable=no [E1A] Crushable=no [E1S] Crushable=no [E2] Crushable=no [E2S] Crushable=no [E3] Crushable=no [E3N] Crushable=no [E3A] Crushable=no [E3S] Crushable=no [E4] Crushable=no [E4S] Crushable=no [E5] Crushable=no [MEDIC] Crushable=no [ENGINEER] Crushable=no [DOG] Crushable=no [RMBO] Crushable=no [TANYA] Crushable=no [CTECH] Crushable=no [MOEBIUS] Crushable=no [DELPHI] Crushable=no [CHAN] Crushable=no [RAPT] Crushable=no [D1] Crushable=no [D2] Crushable=no [SUE1A] Crushable=no ================================================ FILE: DXMainClient/Resources/INI/Game Options/Veteran Balance Patch.ini ================================================ [CnCNet] Author=Humble/Skylegend/Xme/NME/Mola/Carnage/Cambria/Trooper/Black/Etc./Etc. Website=https://github.com/HumbleTS/Balance-Veteran-Patch/wiki/Balance-Veteran-Summary Version=2.50 [General] VeteranROF=.20 VeteranArmor=.20 VeteranRatio=3.5 VeteranSpeed=.20 VeteranCombat=.20 VeinholeGrowthRate=1 [Powerups] Gas=0,,100 Pod=1, ICBM=1,CHEMISLE Unit=0, Armor=1,ARMOR,2.0 Cloak=1,CLOAK Money=0,MONEY,2000 Speed=1,ARMOR,1.7 Squad=0, Napalm=0,,600 Reveal=0,REVEAL Veteran=1,VETERAN,1 Darkness=0,SHROUDX HealBase=0,HEALALL IonStorm=0, Tiberium=0, Explosion=0,,500 Firepower=1,FIREPOWR,2.0 Invulnerability=0,ARMOR,1.0 [E1] Trainable=no [E2] Trainable=no [E3] Trainable=no [CYBORG] Trainable=no [JUMPJET] Trainable=no [GHOST] Elite=EliteLtRail Trainable=yes EliteAbilities=SELF_HEAL,FASTER [CYC2] Elite=EliteCyCannon Trainable=yes EliteAbilities=SELF_HEAL,FASTER [DOGGIE] Cost=330 Elite=EliteFiendShard EliteAbilities=STRONGER,SELF_HEAL [SMECH] Elite=EliteAssaultCannon EliteAbilities=FIREPOWER,STRONGER [BGGY] Elite=EliteRaiderCannon EliteAbilities=FIREPOWER,STRONGER [BIKE] Elite=EliteBikeMissile EliteAbilities=FIREPOWER,STRONGER [MMCH] Elite=Elite120mm EliteAbilities=FIREPOWER,STRONGER [TTNK] Armor=heavy Elite=Elite90mm EliteAbilities=FIREPOWER,STRONGER TooBigToFitUnderBridge=true [GATICK] Elite=Elite90mm EliteAbilities=FIREPOWER,STRONGER [ART2] Elite=Elite155mm EliteAbilities=FIREPOWER,STRONGER TooBigToFitUnderBridge=true [GAARTY] Elite=Elite155mm EliteAbilities=FIREPOWER,STRONGER [HVR] Elite=EliteHoverMissile EliteAbilities=FIREPOWER,STRONGER [STNK] Elite=EliteDragon Armour=wood Turret=Yes Strength=230 CloakingSpeed=3 EliteAbilities=STRONGER,SELF_HEAL [SONIC] Trainable=no [HMEC] Elite=EliteMechRailgun Primary=MechRailgun Secondary=MammothTusk Trainable=yes EliteAbilities=STRONGER,FASTER [WEED] ThreatAvoidanceCoefficient=0.00 [VISC_LRG] Cost=330 Elite=EliteSlimeAttack Trainable=Yes EliteAbilities=STRONGER,SELF_HEAL [ORCA] Elite=EliteHellfire EliteAbilities=FIREPOWER,SELF_HEAL [APACHE] Elite=EliteHarpyClaw EliteAbilities=FIREPOWER,SELF_HEAL [ORCAB] Elite=EliteBomb EliteAbilities=FIREPOWER,SELF_HEAL [SCRIN] Elite=EliteProton EliteAbilities=FIREPOWER,SELF_HEAL [GAGATE_A] DeployTime=0.022 GateCloseDelay=0.044 [GAGATE_B] DeployTime=0.022 GateCloseDelay=0.044 [GAPAVE] Adjacent=5 [GAPLUG2] Prerequisite=GAPLUG [GAPLUG3] Prerequisite=GAPLUG [NAGATE_A] DeployTime=0.022 GateCloseDelay=0.044 [NAGATE_B] DeployTime=0.022 GateCloseDelay=0.044 [NAOBEL] Armor=heavy Strength=600 [NAWAST] Adjacent=5 Strength=1200 [NAPAVE] Adjacent=5 [AssaultCannon] Burst=2 Damage=31 [CyCannon] Speed=35 [Heal] Burst=2 Range=4.5 [LaserFire] ROF=46 [ProtonBlast] Ranged=no Proximity=no Acceleration=12 [Vulcan3] Damage=11 [EliteAssaultCannon] ROF=25 Burst=2 Range=6 Speed=100 Damage=37 Report=TSGUN4 Warhead=SA Projectile=Invisible [EliteRaiderCannon] Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW Range=4.8 Speed=100 Damage=48 Report=CHAINGN1 Warhead=SA Projectile=Invisible [Elite120mm] ROF=80 Anim=GUNFIRE Range=8.1 Speed=90 Bright=yes Damage=84 Report=120MMF Warhead=AP Projectile=Invisible [Elite155mm] ROF=110 Anim=GUNFIRE Range=21.60 Speed=10 Damage=180 Lobber=yes Report=120MMF Warhead=ARTYHE Projectile=Ballistic MinimumRange=5.00 [Elite90mm] ROF=50 Anim=GUNFIRE Range=8.1 Speed=40 Bright=yes Damage=43 Report=120MMF Warhead=AP Projectile=Cannon [EliteBikeMissile] ROF=60 Range=6 Speed=30 Damage=48 Report=MISL1 Warhead=AP Projectile=HeatSeeker [EliteBomb] ROF=10 Range=5 Speed=0 Damage=192 Floater=yes Warhead=ORCAHE Projectile=Cannon2 [EliteCyCannon] ROF=50 Range=8.4 Speed=35 Damage=180 Report=scrin5b Warhead=PlasmaWH Projectile=ProtonBlast [EliteDragon] ROF=50 Burst=2 Range=7.2 Speed=25 Damage=36 Report=MISL1 Warhead=AP Projectile=AAHeatSeeker2 [EliteFiendShard] ROF=30 Burst=3 Range=6 Speed=25 Damage=42 Report=FIEND2 Warhead=Shard Projectile=DogShard [EliteHarpyClaw] ROF=36 Range=5 Speed=100 Damage=72 Report=CYGUN1 Warhead=SA Projectile=Invisible2 [EliteHellfire] ROF=50 Burst=2 Range=6 Speed=30 Damage=36 Report=ORCAMIS1 Warhead=ORCAAP Projectile=AAHeatSeeker2 [EliteHoverMissile] ROF=68 Burst=2 Range=9.6 Speed=30 Damage=36 Report=HOVRMIS1 Warhead=AP Projectile=AAHeatSeeker2 MinimumRange=2 [EliteLtRail] ROF=60 Anim=GUNFIRE Range=7.2 Speed=100 Damage=0 Report=BIGGGUN1 Warhead=RailShot2 IsRailgun=true Projectile=Invisible AmbientDamage=180 AttachedParticleSystem=SmallRailgunSys [EliteMechRailgun] ROF=60 Anim=GUNFIRE Range=9.6 Speed=100 Damage=0 Report=RAILUSE5 Warhead=RailShot IsRailgun=true Projectile=Invisible AmbientDamage=300 AttachedParticleSystem=LargeRailgunSys [EliteProton] ROF=3 Range=5 Speed=30 Damage=24 Report=scrin5b Warhead=AP Projectile=ProtonTorpedo [EliteSlimeAttack] ROF=80 Range=1.56 Speed=25 Damage=120 Report=VICER1 Warhead=Slimer Projectile=Invisible [ORCAHE] Verses=200%,90%,75%,32%,32% [IonWH] Verses=100%,90%,75%,60%,25% ================================================ FILE: DXMainClient/Resources/INI/MPMaps.ini ================================================ [MultiMaps] 0=MAPS\TIBERIAN SUN\TERRACE 1=MAPS\FIRESTORM\DUEL 2=MAPS\FAN-MADE\PAIN_REDEFINED 3=MAPS\FAN-MADE\COLD_WAR [GameModeAliases] Standard=Custom Map standard=Custom Map [MAPS\TIBERIAN SUN\TERRACE] CD=0,1,2 ;MinPlayers=1 MaxPlayers=4 Description=[4] Terrace Author=Westwood Studios EnforceMaxPlayers=False Size=0,0,120,120 LocalSize=2,4,116,109 PreviewSize=800,386 GameModes=Default Waypoint0=118035 Waypoint1=110188 Waypoint2=199114 Waypoint3=29130 [MAPS\FIRESTORM\DUEL] CD=0,1,2 ;MinPlayers=1 MaxPlayers=2 Description=[2] FS Dueling Islands Author=Westwood Studios EnforceMaxPlayers=False Size=0,0,88,69 LocalSize=0,0,88,69 PreviewSize=800,302 GameModes=Default Waypoint0=107064 Waypoint1=58083 [MAPS\FAN-MADE\PAIN_REDEFINED] CD=0,1,2 ;MinPlayers=1 MaxPlayers=2 Description=[2] Pain Redefined Author=Aro EnforceMaxPlayers=False Size=0,0,65,80 LocalSize=2,4,61,69 PreviewSize=800,482 GameModes=Fan-made Waypoint0=111087 Waypoint1=68031 [MAPS\FAN-MADE\COLD_WAR] CD=0,1,2 ;MinPlayers=1 MaxPlayers=3 Description=[3] Cold War Author=Aro EnforceMaxPlayers=False Size=0,0,75,88 LocalSize=2,4,71,78 PreviewSize=800,465 GameModes=Fan-made Waypoint0=51047 Waypoint1=136094 Waypoint2=88128 ================================================ FILE: DXMainClient/Resources/INI/Map Code/Difficulty Easy.ini ================================================ [Events] DifficultyGlobal=1,8,0,0 [Actions] DifficultyGlobal=2,28,0,12,0,0,0,0,A,28,0,15,0,0,0,0,A [Triggers] DifficultyGlobal=Neutral,,Set Difficulty Global,0,1,1,1,0 [Tags] DifficultyGlobalTag=0,Set Difficulty Global 1,DifficultyGlobal ================================================ FILE: DXMainClient/Resources/INI/Map Code/Difficulty Hard.ini ================================================ [Events] DifficultyGlobal=1,8,0,0 [Actions] DifficultyGlobal=2,28,0,14,0,0,0,0,A,28,0,16,0,0,0,0,A [Triggers] DifficultyGlobal=Neutral,,Set Difficulty Global,0,1,1,1,0 [Tags] DifficultyGlobalTag=0,Set Difficulty Global 1,DifficultyGlobal ================================================ FILE: DXMainClient/Resources/INI/Map Code/Difficulty Medium.ini ================================================ [Events] DifficultyGlobal=1,8,0,0 [Actions] DifficultyGlobal=3,28,0,13,0,0,0,0,A,28,0,15,0,0,0,0,A,28,0,16,0,0,0,0,A [Triggers] DifficultyGlobal=Neutral,,Set Difficulty Global,0,1,1,1,0 [Tags] DifficultyGlobalTag=0,Set Difficulty Global 1,DifficultyGlobal ================================================ FILE: DXMainClient/Resources/INI/Map Code/King of the Hill.ini ================================================ [Tags] ShortGameTimerTag=0,Short Game Timer 1,ShortGameTimer ================================================ FILE: DXMainClient/Resources/INI/Map Code/Naval Only AI.ini ================================================ [Basic] IgnoreGlobalAITriggers=yes [General] MultiplayerAICM=2147483640,2147483640,2147483640 [HPAD_AI] Prerequisite=NSYRD_AI [AHPAD_AI] Prerequisite=ASYRD_AI [GSYRD_AI] Prerequisite=none [NSYRD_AI] Prerequisite=none [ASYRD_AI] Prerequisite=none [RASPEN_AI] Prerequisite=none [AITriggerTypes] 08507720-G=E_GDI replace MCV,084ECBA0-G,,7,1,GFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,,1,0,0 091F4720-G=E_Nod replace MCV,084EC720-G,,7,1,NFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,,1,0,0 08B80660-G=M_GDI replace MCV,084EC7FM-G,,7,1,GFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,,0,1,0 0930A240-G=M_Nod replace MCV,084EC65M-G,,7,1,NFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,,0,1,0 0930BDC0-G=H_Nod replace MCV,084EC650-G,,7,1,NFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,,0,0,1 08B80760-G=H_GDI replace MCV,084EC7F0-G,,7,1,GFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,,0,0,1 08507720-S=E_Soviet replace MCV,084ECBA0-S,,7,1,SFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,4,0,,1,0,0 08B80660-S=M_Soviet replace MCV,084EC7FM-S,,7,1,SFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,4,0,,0,1,0 08B80760-S=H_Soviet replace MCV,084EC7F0-S,,7,1,SFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,4,0,,0,0,1 08507720-A=E_Allies replace MCV,084ECBA0-A,,7,1,AFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,3,0,,1,0,0 08B80660-A=M_Allies replace MCV,084EC7FM-A,,7,1,AFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,3,0,,0,1,0 08B80760-A=H_Allies replace MCV,084EC7F0-A,,7,1,AFACT,0100000005000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,3,0,,0,0,1 012345678-G=MCV GDI Deploy,MCV_GDI_DEP,,1,1,GFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,1,0,,1,1,1 012345678-N=MCV Nod Deploy,MCV_NOD_DEP,,1,1,NFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,2,0,,1,1,1 012345678-A=MCV Allies Deploy,MCV_ALI_DEP,,1,1,AFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,3,0,,1,1,1 012345678-S=MCV Soviet Deploy,MCV_SOV_DEP,,1,1,SFACT,0100000000000000000000000000000000000000000000000000000000000000,5000.000000,5000.000000,5000.000000,1,0,4,0,,1,1,1 0859C820-G=E_GDI vehicle attack 2,080CF780-G,,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF6B0-G,1,0,0 0859C720-G=E_GDI vehicle attack 3,080CF520-G,,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF520-G,1,0,0 09103C20-G=E_GDI hover pool,090EE280-G,,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE280-G,1,0,0 08440AE0-G=E_GDI base air defense 5,084E5B50-G,,7,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0 0859C820-A=E_Allies vehicle attack 2,080CF780-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,080CF6B0-A,1,0,0 0859C720-A=E_Allies vehicle attack 3,080CF520-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,080CF520-A,1,0,0 09103C20-A=E_Allies hover pool,090EE280-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,090EE280-A,1,0,0 08440AE0-A=E_Allies base air defense 5,084E5B50-A,,7,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,3,0,084E5A80-A,1,0,0 07CC6F10-A=E_Allies juggernaut pool,09E4FF40-A,,3,1,RADOME_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,09E4FF40-A,1,0,0 090F8D20-A=H_Allies hover pool,090E0620-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,3,1,090E0620-A,0,0,1 08596C20-A=H_Allies vehicle attack 2,0753BE90-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,3,0,073A8540-A,0,0,1 08596B20-A=H_Allies vehicle attack 3,07EA0540-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,3,0,,0,0,1 084EF820-A=H_Allies ORCA bomber pool,0B7D44A0-A,,5,1,RADOME_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,3,1,,0,0,1 0859A160-A=H_Allies missile silo attack 1,084DE340-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080F71A0-A,0,0,1 0859BE20-A=H_Allies missile silo attack 4,080CCD80-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080CCCA0-A,0,0,1 0859B330-A=H_Allies upgrade center attack 1,084E0F50-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080CEA80-A,0,0,1 0859CE20-A=H_Allies upgrade center attack 4,080CE620-A,,7,1,AWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,3,0,080CE540-A,0,0,1 084712B0-A=H_Allies power facility attack 1,073AE790-A,,7,2,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,3,0,041742A0-A,0,0,1 07CC6F10-S=E_Soviet juggernaut pool,09E4FF40-S,,3,1,RADOME_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,4,0,09E4FF40-S,1,0,0 07CC6580-S=E_Soviet base defense attack 3,08820200-S,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,4,0,08820200-S,1,0,0 07CC7F10-S=E_Soviet factories attack 7,09E4A280-S,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,4,0,09E4A280-S,1,0,0 07CC7BE0-S=E_Soviet missile silo attack 5,09E4FD80-S,,3,0,TMPL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,4,0,09E4FD80-S,1,0,0 07CC78B0-S=E_Soviet power facilities attack 5,041A7750-S,,3,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,4,0,041A7750-S,1,0,0 07CC6800-S=E_Soviet upgrade center attack 5,07CC69F0-S,,3,0,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,4,0,07CC69F0-S,1,0,0 090F8D20-S=H_Soviet hover pool,090E0620-S,,7,1,SWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,4,1,090E0620-S,0,0,1 08596C20-S=H_Soviet vehicle attack 2,0753BE90-S,,7,1,SWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,4,0,073A8540-S,0,0,1 08596B20-S=H_Soviet vehicle attack 3,07EA0540-S,,7,1,SWEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,4,0,,0,0,1 080D1820-G=M_GDI hover pool,090E062M-G,,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D3A20-G=M_GDI vehicle attack 2,0753BE9M-G,,7,1,WEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073A854M-G,0,1,0 085A22A0-G=E_Nod aerial base attack 1,09D00530-G,,5,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,09D00530-G,1,0,0 085A21A0-G=E_Nod aerial base attack 2,08607590-G,,7,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,08607590-G,1,0,0 085A20A0-G=E_Nod aerial vehicle attack ,086074C0-G,,7,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086074C0-G,1,0,0 080C71E0-G=M_GDI aerial harvester attack,0B7F742M-G,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080C71E1-G=M_GDI aerial harvester attack,0B7F742M-G,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080C71E2-G=M_GDI aerial harvester attack,0B7F742M-G,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080C71E3-G=M_GDI aerial harvester attack,0B7F742M-G,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080D1C20-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080D1C21-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080D1C22-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080D1C23-G=M_GDI aerial harvesters attack 4,0B7F46FM-G,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,1,0,,0,1,0 080D2620-G=M_GDI aerial harvester attack 4,041768BM-G,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,,0,1,0 080D2621-G=M_GDI aerial harvester attack 4,041768BM-G,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,,0,1,0 080D2622-G=M_GDI aerial harvester attack 4,041768BM-G,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,,0,1,0 080D2623-G=M_GDI aerial harvester attack 4,041768BM-G,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,1,0,,0,1,0 080D2220-G=M_GDI aerial harvester attack 4,04174D70-G,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 080D2221-G=M_GDI aerial harvester attack 4,04174D70-G,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 080D2222-G=M_GDI aerial harvester attack 4,04174D70-G,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 080D2223-G=M_GDI aerial harvester attack 4,04174D70-G,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 080D4F24-G=M_Nod aerial base attack 1,0B7D48EM-G,,5,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,30.000000,40.000000,1,0,2,0,,0,1,0 080D4E20-G=M_Nod aerial harvester attack 2,0B7D473M-G,,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,,0,1,0 080D4E21-G=M_Nod aerial harvester attack 2,0B7D473M-G,,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,,0,1,0 080D4E22-G=M_Nod aerial harvester attack 2,0B7D473M-G,,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,,0,1,0 080D4E23-G=M_Nod aerial harvester attack 2,0B7D473M-G,,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,,0,1,0 080D4D20-G=M_Nod aerial vehicle attack,0B7F750M-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,2,0,,0,1,0 084EFB20-G=H_Nod aerial base attack 1,0B7D48E0-G,,5,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,50.000000,60.000000,1,0,2,0,,0,0,1 084EFA20-G=H_Nod aerial harvester attack 2,0B7D4730-G,,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,,0,0,1 084EFA21-G=H_Nod aerial harvester attack 2,0B7D4730-G,,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,,0,0,1 084EFA22-G=H_Nod aerial harvester attack 2,0B7D4730-G,,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,,0,0,1 084EFA23-G=H_Nod aerial harvester attack 2,0B7D4730-G,,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,2,0,,0,0,1 084F0B20-G=H_Nod aerial vehicle attack,0B7F7500-G,,9,1,HPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,0,1 084EFF20-G=H_GDI aerial harvester attack 1,0B7D4E70-G,,5,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,,0,0,1 084EFF21-G=H_GDI aerial harvester attack 1,0B7D4E70-G,,5,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,,0,0,1 084EFF22-G=H_GDI aerial harvester attack 1,0B7D4E70-G,,5,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,,0,0,1 084EFF23-G=H_GDI aerial harvester attack 1,0B7D4E70-G,,5,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,1,0,,0,0,1 084F0A20-G=H_GDI aerial harvester attack,0B7F7420-G,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,,0,0,1 084F0A21-G=H_GDI aerial harvester attack,0B7F7420-G,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,,0,0,1 084F0A22-G=H_GDI aerial harvester attack,0B7F7420-G,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,,0,0,1 084F0A23-G=H_GDI aerial harvester attack,0B7F7420-G,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,,0,0,1 085983D0-G=H_GDI aerial harvester attack 4,0B7F46F0-G,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1 085983D1-G=H_GDI aerial harvester attack 4,0B7F46F0-G,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1 085983D2-G=H_GDI aerial harvester attack 4,0B7F46F0-G,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1 085983D3-G=H_GDI aerial harvester attack 4,0B7F46F0-G,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,40.000000,70.000000,1,0,1,0,0B7F4610-G,0,0,1 075A52F0-G=H_GDI aerial harvester attack 4,04174D70-G,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1 075A52F1-G=H_GDI aerial harvester attack 4,04174D70-G,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1 075A52F2-G=H_GDI aerial harvester attack 4,04174D70-G,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1 075A52F3-G=H_GDI aerial harvester attack 4,04174D70-G,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,55.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1 08472E20-G=H_GDI aerial harvester attack 4,041768B0-G,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1 08472E21-G=H_GDI aerial harvester attack 4,041768B0-G,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1 08472E22-G=H_GDI aerial harvester attack 4,041768B0-G,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1 08472E23-G=H_GDI aerial harvester attack 4,041768B0-G,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,1,0,041767D0-G,0,0,1 0859E270-S=E_Soviet aerial base attack 1,07F2A9F0-S,,5,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,4,0,07F2A9F0-S,1,0,0 0859E170-S=E_Soviet aerial base attack 2,07F2A920-S,,7,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,4,0,07EF88B0-S,1,0,0 0859E070-S=E_Soviet aerial vehicle attack,07F2A850-S,,5,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,4,0,07F2A850-S,1,0,0 080CF220-S=M_Soviet aerial base attack 1,0B7D4E7M-S,,5,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,4,0,,0,1,0 080CF120-S=M_Soviet aerial base attack 2,0B7D4A9M-S,,7,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,4,0,,0,1,0 080C71E0-S=M_Soviet aerial harvester attack,0B7F742M-S,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080C71E1-S=M_Soviet aerial harvester attack,0B7F742M-S,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080C71E2-S=M_Soviet aerial harvester attack,0B7F742M-S,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080C71E3-S=M_Soviet aerial harvester attack,0B7F742M-S,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080D1C20-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080D1C21-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080D1C22-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080D1C23-S=M_Soviet aerial harvesters attack 4,0B7F46FM-S,,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,4,0,,0,1,0 080D2620-S=M_Soviet aerial harvester attack 4,041768BM-S,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,,0,1,0 080D2621-S=M_Soviet aerial harvester attack 4,041768BM-S,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,,0,1,0 080D2622-S=M_Soviet aerial harvester attack 4,041768BM-S,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,,0,1,0 080D2623-S=M_Soviet aerial harvester attack 4,041768BM-S,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,4,0,,0,1,0 080D2220-S=M_Soviet aerial harvester attack 4,04174D70-S,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,,0,1,0 080D2221-S=M_Soviet aerial harvester attack 4,04174D70-S,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,,0,1,0 080D2222-S=M_Soviet aerial harvester attack 4,04174D70-S,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,,0,1,0 080D2223-S=M_Soviet aerial harvester attack 4,04174D70-S,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,4,0,,0,1,0 084EFF20-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,,0,0,1 084EFF21-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,,0,0,1 084EFF22-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,,0,0,1 084EFF23-S=H_Soviet aerial harvester attack 1,0B7D4E70-S,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,4,0,,0,0,1 084EFC20-S=H_Soviet aerial base attack 2,0B7D4A90-S,,7,-1,RAASTRP,0100000003000000000000000000000000000000000000000000000000000000,45.000000,35.000000,55.000000,1,0,4,0,,0,0,1 084F0A20-S=H_Soviet aerial harvester attack,0B7F7420-S,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,,0,0,1 084F0A21-S=H_Soviet aerial harvester attack,0B7F7420-S,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,,0,0,1 084F0A22-S=H_Soviet aerial harvester attack,0B7F7420-S,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,,0,0,1 084F0A23-S=H_Soviet aerial harvester attack,0B7F7420-S,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,,0,0,1 085983D0-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,,8,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1 085983D1-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,,8,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1 085983D2-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,,8,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1 085983D3-S=H_Soviet aerial harvester attack 4,0B7F46F0-S,,8,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,4,0,0B7F4610-S,0,0,1 075A52F0-S=H_Soviet aerial harvester attack 4,04174D70-S,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1 075A52F1-S=H_Soviet aerial harvester attack 4,04174D70-S,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1 075A52F2-S=H_Soviet aerial harvester attack 4,04174D70-S,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1 075A52F3-S=H_Soviet aerial harvester attack 4,04174D70-S,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,4,0,04174BB0-S,0,0,1 08472E20-S=H_Soviet aerial harvester attack 4,041768B0-S,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1 08472E21-S=H_Soviet aerial harvester attack 4,041768B0-S,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1 08472E22-S=H_Soviet aerial harvester attack 4,041768B0-S,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1 08472E23-S=H_Soviet aerial harvester attack 4,041768B0-S,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,4,0,041767D0-S,0,0,1 0859E270-A=E_Allies aerial base attack 1,07F2A9F0-A,,5,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,3,0,07F2A9F0-A,1,0,0 0859E170-A=E_Allies aerial base attack 2,07F2A920-A,,7,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,3,0,07EF88B0-A,1,0,0 0859E070-A=E_Allies aerial vehicle attack,07F2A850-A,,5,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,3,0,07F2A850-A,1,0,0 080CF220-A=M_Allies aerial base attack 1,0B7D4E7M-A,,5,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,3,0,,0,1,0 080CF120-A=M_Allies aerial base attack 2,0B7D4A9M-A,,7,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,3,0,,0,1,0 080C71E0-A=M_Allies aerial harvester attack,0B7F742M-A,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080C71E1-A=M_Allies aerial harvester attack,0B7F742M-A,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080C71E2-A=M_Allies aerial harvester attack,0B7F742M-A,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080C71E3-A=M_Allies aerial harvester attack,0B7F742M-A,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080D1C20-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,,7,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080D1C21-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,,7,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080D1C22-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,,7,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080D1C23-A=M_Allies aerial harvesters attack 4,0B7F46FM-A,,7,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,50.000000,90.000000,1,0,3,0,,0,1,0 080D2620-A=M_Allies aerial harvester attack 4,041768BM-A,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,,0,1,0 080D2621-A=M_Allies aerial harvester attack 4,041768BM-A,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,,0,1,0 080D2622-A=M_Allies aerial harvester attack 4,041768BM-A,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,,0,1,0 080D2623-A=M_Allies aerial harvester attack 4,041768BM-A,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,20.000000,30.000000,1,0,3,0,,0,1,0 080D2220-A=M_Allies aerial harvester attack 4,04174D70-A,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,,0,1,0 080D2221-A=M_Allies aerial harvester attack 4,04174D70-A,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,,0,1,0 080D2222-A=M_Allies aerial harvester attack 4,04174D70-A,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,,0,1,0 080D2223-A=M_Allies aerial harvester attack 4,04174D70-A,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,3,0,,0,1,0 084EFF20-A=H_Allies aerial harvester attack 1,0B7D4E70-A,,5,-1,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,,0,0,1 084EFF21-A=H_Allies aerial harvester attack 1,0B7D4E70-A,,5,-1,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,,0,0,1 084EFF22-A=H_Allies aerial harvester attack 1,0B7D4E70-A,,5,-1,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,,0,0,1 084EFF23-A=H_Allies aerial harvester attack 1,0B7D4E70-A,,5,-1,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,30.000000,60.000000,1,0,3,0,,0,0,1 084EFC20-A=H_Allies aerial base attack 2,0B7D4A90-A,,7,-1,AHPAD_AI,0100000003000000000000000000000000000000000000000000000000000000,45.000000,35.000000,55.000000,1,0,3,0,,0,0,1 084F0A20-A=H_Allies aerial harvester attack,0B7F7420-A,,5,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,,0,0,1 084F0A21-A=H_Allies aerial harvester attack,0B7F7420-A,,5,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,,0,0,1 084F0A22-A=H_Allies aerial harvester attack,0B7F7420-A,,5,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,,0,0,1 084F0A23-A=H_Allies aerial harvester attack,0B7F7420-A,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,,0,0,1 085983D0-A=H_Allies aerial harvester attack 4,0B7F46F0-A,,8,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1 085983D1-A=H_Allies aerial harvester attack 4,0B7F46F0-A,,8,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1 085983D2-A=H_Allies aerial harvester attack 4,0B7F46F0-A,,8,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1 085983D3-A=H_Allies aerial harvester attack 4,0B7F46F0-A,,8,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,50.000000,40.000000,60.000000,1,0,3,0,0B7F4610-A,0,0,1 075A52F0-A=H_Allies aerial harvester attack 4,04174D70-A,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1 075A52F1-A=H_Allies aerial harvester attack 4,04174D70-A,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1 075A52F2-A=H_Allies aerial harvester attack 4,04174D70-A,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1 075A52F3-A=H_Allies aerial harvester attack 4,04174D70-A,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,45.000000,10.000000,70.000000,1,0,3,0,04174BB0-A,0,0,1 08472E20-A=H_Allies aerial harvester attack 4,041768B0-A,,7,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1 08472E21-A=H_Allies aerial harvester attack 4,041768B0-A,,7,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1 08472E22-A=H_Allies aerial harvester attack 4,041768B0-A,,7,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1 08472E23-A=H_Allies aerial harvester attack 4,041768B0-A,,7,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,30.000000,50.000000,1,0,3,0,041767D0-A,0,0,1 ; anti-naval yard triggers GDINAVAG-G=HM_GDI Aerial GDI Naval Yard Attack,GDIAANGT-G,,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,1,0,,0,1,1 ;GDINAVHG-G=HM_GDI Hovering GDI Naval Yard Attack,GDIHANAV-G,,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,1,0,,0,1,1 NODNAVAG-G=H_Nod Aerial GDI Naval Yard Attack,NODAANVT-G,,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,1000.000000,1000.000000,1000.000000,1,0,2,0,,0,0,1 ;NODNAVHG-G=HM_Nod Hovering GDI Naval Yard Attack,NODHANVT-G,,-1,0,GSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,2,0,,0,1,1 GDINAVAN-G=HM_GDI Aerial Nod Naval Yard Attack,GDIAANNT-G,,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,1,0,,0,1,1 ;GDINAVHN-G=HM_GDI Hovering Nod Naval Yard Attack,GDIHANNT-G,,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,1,0,,0,1,1 NODNAVAN-G=H_Nod Aerial Nod Naval Yard Attack,NODAANNT-G,,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,2,0,,0,0,1 ;NODNAVHN-G=HM_Nod Hovering Nod Naval Yard Attack,NODHANNT-G,,-1,0,NSYRD,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,2,0,,0,1,1 GDINAVAS-G=HM_GDI Aerial Soviet Naval Yard Attack,GDIAANST-G,,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,1,0,,0,1,1 ;GDINAVHS-G=HM_GDI Hovering Soviet Naval Yard Attack,GDIHANST-G,,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,1,0,,0,1,1 NODNAVAS-G=H_Nod Aerial Soviet Naval Yard Attack,NODAANST-G,,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,10000.000000,10000.000000,10000.000000,1,0,2,0,,0,0,1 ;NODNAVHS-G=HM_Nod Hovering Soviet Naval Yard Attack,NODHANST-G,,-1,0,RASPEN,0100000003000000000000000000000000000000000000000000000000000000,50000.000000,50000.000000,50000.000000,1,0,2,0,,0,1,1 ; ship triggers NANVLASE-G=E_Nod Naval Patrol 1,NNVLASET-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,,1,0,0 NANVLABE-G=E_Nod Naval Building Attack 1,NNVLABET-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,,1,0,0 NANVLS2E-G=E_Nod Naval Patrol 2,NNVLASET-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,,1,0,0 NANVLB2E-G=E_Nod Naval Building Attack 2,NNVLABET-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,,1,0,0 NANVLS3E-G=E_Nod Naval Patrol 3,NNVLASET-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,,1,0,0 NANVLB3E-G=E_Nod Naval Building Attack 3,NNVLABET-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,2,0,,1,0,0 NANVLASM-G=M_Nod Naval Patrol 1,NNVLASMT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,2,0,,0,1,0 NANVLABM-G=M_Nod Naval Building Attack,NNVLABMT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,2,0,,0,1,0 NANVLS2M-G=M_Nod Naval Patrol 2,NNVLASMT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,2,0,,0,1,0 NANVLB2M-G=M_Nod Naval Building Attack 2,NNVLABMT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,2,0,,0,1,0 NANVLS3M-G=M_Nod Naval Patrol 3,NNVLASMT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,2,0,,0,1,0 NANVLB3M-G=M_Nod Naval Building Attack 3,NNVLABMT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,2,0,,0,1,0 NANVLASH-G=H_Nod Naval Patrol 1,NNVLASHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,,0,0,1 NANVLABH-G=H_Nod Naval Building Attack 1,NNVLABHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,,0,0,1 NANVLS2H-G=H_Nod Naval Patrol 2,NNVLASHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,,0,0,1 NANVLB2H-G=H_Nod Naval Building Attack 2,NNVLABHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,,0,0,1 NANVLS3H-G=H_Nod Naval Patrol 3,NNVLASHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,,0,0,1 NANVLB3H-G=H_Nod Naval Building Attack 3,NNVLABHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,,0,0,1 NANVLS4H-G=H_Nod Naval Patrol 4,NNVLASHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,2,0,,0,0,1 NANVLB4H-G=H_Nod Naval Building Attack 4,NNVLABHT-G,,-1,1,NSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,2,0,,0,0,1 GANVLASE-G=E_GDI Naval Patrol 1,GNVLASET-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,,1,0,0 GANVLABE-G=E_GDI Naval Building Attack 1,GNVLABET-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,,1,0,0 GANVLS2E-G=E_GDI Naval Patrol 2,GNVLASET-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,,1,0,0 GANVLB2E-G=E_GDI Naval Building Attack 2,GNVLABET-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,,1,0,0 GANVLS3E-G=E_GDI Naval Patrol 3,GNVLASET-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,,1,0,0 GANVLB3E-G=E_GDI Naval Building Attack 3,GNVLABET-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,1,0,,1,0,0 GANVLASM-G=M_GDI Naval Patrol 1,GNVLASMT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,1,0,,0,1,0 GANVLABM-G=M_GDI Naval Building Attack 1,GNVLABMT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,1,0,,0,1,0 GANVLS2M-G=M_GDI Naval Patrol 2,GNVLASMT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,1,0,,0,1,0 GANVLB2M-G=M_GDI Naval Building Attack 2,GNVLABMT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,1,0,,0,1,0 GANVLS3M-G=M_GDI Naval Patrol 3,GNVLASMT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,1,0,,0,1,0 GANVLB3M-G=M_GDI Naval Building Attack 3,GNVLABMT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,1,0,,0,1,0 GANVLASH-G=H_GDI Naval Patrol 1,GNVLASHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,,0,0,1 GANVLABH-G=H_GDI Naval Building Attack 1,GNVLABHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,,0,0,1 GANVLS2H-G=H_GDI Naval Patrol 2,GNVLASHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,,0,0,1 GANVLB2H-G=H_GDI Naval Building Attack 2,GNVLABHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,,0,0,1 GANVLS3H-G=H_GDI Naval Patrol 3,GNVLASHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,,0,0,1 GANVLB3H-G=H_GDI Naval Building Attack 3,GNVLABHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,,0,0,1 GANVLS4H-G=H_GDI Naval Patrol 4,GNVLASHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,1,0,,0,0,1 GANVLB4H-G=H_GDI Naval Building Attack 4,GNVLABHT-G,,-1,1,GSYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,1,0,,0,0,1 SANVLASE-G=E_Soviet Naval Patrol 1,SNVLASET-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,,1,0,0 SANVLABE-G=E_Soviet Naval Building Attack 1,SNVLABET-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,,1,0,0 SANVLS2E-G=E_Soviet Naval Patrol 2,SNVLASET-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,,1,0,0 SANVLB2E-G=E_Soviet Naval Building Attack 2,SNVLABET-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,,1,0,0 SANVLS3E-G=E_Soviet Naval Patrol 3,SNVLASET-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,,1,0,0 SANVLB3E-G=E_Soviet Naval Building Attack 3,SNVLABET-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,4,0,,1,0,0 SANVLASM-G=M_Soviet Naval Patrol 1,SNVLASMT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,4,0,,0,1,0 SANVLABM-G=M_Soviet Naval Building Attack 1,SNVLABMT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,4,0,,0,1,0 SANVLS2M-G=M_Soviet Naval Patrol 2,SNVLASMT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,4,0,,0,1,0 SANVLB2M-G=M_Soviet Naval Building Attack 2,SNVLABMT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,4,0,,0,1,0 SANVLS3M-G=M_Soviet Naval Patrol 3,SNVLASMT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,4,0,,0,1,0 SANVLB3M-G=M_Soviet Naval Building Attack 3,SNVLABMT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,4,0,,0,1,0 SANVLASH-G=H_Soviet Naval Patrol 1,SNVLASHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,,0,0,1 SANVLABH-G=H_Soviet Naval Building Attack 1,SNVLABHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,,0,0,1 SANVLS2H-G=H_Soviet Naval Patrol 2,SNVLASHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,,0,0,1 SANVLB2H-G=H_Soviet Naval Building Attack 2,SNVLABHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,,0,0,1 SANVLS3H-G=H_Soviet Naval Patrol 3,SNVLASHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,,0,0,1 SANVLB3H-G=H_Soviet Naval Building Attack 3,SNVLABHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,,0,0,1 SANVLS4H-G=H_Soviet Naval Patrol 4,SNVLASHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,4,0,,0,0,1 SANVLB4H-G=H_Soviet Naval Building Attack 4,SNVLABHT-G,,-1,1,RASPEN_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,4,0,,0,0,1 AANVLASE-G=E_Allies Naval Patrol 1,ANVLASET-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,,1,0,0 AANVLABE-G=E_Allies Naval Building Attack 1,ANVLABET-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,,1,0,0 AANVLS2E-G=E_Allies Naval Patrol 2,ANVLASET-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,,1,0,0 AANVLB2E-G=E_Allies Naval Building Attack 2,ANVLABET-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,,1,0,0 AANVLS3E-G=E_Allies Naval Patrol 3,ANVLASET-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,,1,0,0 AANVLB3E-G=E_Allies Naval Building Attack 3,ANVLABET-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,100.000000,20.000000,120.000000,1,0,3,0,,1,0,0 AANVLASM-G=M_Allies Naval Patrol 1,ANVLASMT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,3,0,,0,1,0 AANVLABM-G=M_Allies Naval Building Attack 1,ANVLABMT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,3,0,,0,1,0 AANVLS2M-G=M_Allies Naval Patrol 2,ANVLASMT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,3,0,,0,1,0 AANVLB2M-G=M_Allies Naval Building Attack 2,ANVLABMT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,3,0,,0,1,0 AANVLS3M-G=M_Allies Naval Patrol 3,ANVLASMT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,120.000000,20.000000,160.000000,1,0,3,0,,0,1,0 AANVLB3M-G=M_Allies Naval Building Attack 3,ANVLABMT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,140.000000,40.000000,180.000000,1,0,3,0,,0,1,0 AANVLASH-G=H_Allies Naval Patrol 1,ANVLASHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,,0,0,1 AANVLABH-G=H_Allies Naval Building Attack 1,ANVLABHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,,0,0,1 AANVLS2H-G=H_Allies Naval Patrol 2,ANVLASHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,,0,0,1 AANVLB2H-G=H_Allies Naval Building Attack 2,ANVLABHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,,0,0,1 AANVLS3H-G=H_Allies Naval Patrol 3,ANVLASHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,,0,0,1 AANVLB3H-G=H_Allies Naval Building Attack 3,ANVLABHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,,0,0,1 AANVLS4H-G=H_Allies Naval Patrol 4,ANVLASHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,525.000000,75.000000,650.000000,1,0,3,0,,0,0,1 AANVLB4H-G=H_Allies Naval Building Attack 4,ANVLABHT-G,,-1,1,ASYRD_AI,0100000003000000000000000000000000000000000000000000000000000000,700.000000,150.000000,750.000000,1,0,3,0,,0,0,1 ;0TRMTHH0-G=H_Nod Termite harvester attack,TRMITETT-G,,-1,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,,0,0,1 ;0TRMTHH1-G=H_Nod Termite harvester attack,TRMITETT-G,,-1,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,,0,0,1 ;0TRMTHH2-G=H_Nod Termite harvester attack,TRMITETT-G,,-1,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,,0,0,1 ;0TRMTHH3-G=H_Nod Termite harvester attack,TRMITETT-G,,-1,0,EMRAHARV,0200000003000000000000000000000000000000000000000000000000000000,110.000000,40.000000,150.000000,1,0,2,0,,0,0,1 ;0TRMTFH1-G=H_Nod Termite Conyard Attack,TRMITF01-G,,-1,0,GFACT,0100000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,,0,0,1 ;0TRMTFH2-G=H_Nod Termite Soviet Conyard Attack,TRMITF02-G,,-1,0,SFACT,0100000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,,0,0,1 ;0TRMTFH3-G=H_Nod Termite Allied Conyard Attack,TRMITF03-G,,-1,0,AFACT,0100000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,,0,0,1 ;0TRMTRH1-G=H_Nod Termite Refinery Attack,TRMITR01-G,,-1,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,,0,0,1 ;0TRMTRH2-G=H_Nod Termite Refinery Attack,TRMITR02-G,,-1,0,RAPROC,0300000003000000000000000000000000000000000000000000000000000000,160.000000,50.000000,180.000000,1,0,2,0,,0,0,1 ;0TRMTMH0-G=M_Nod Termite harvester attack,TRMITETT-G,,-1,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,,0,1,0 ;0TRMTMH1-G=M_Nod Termite harvester attack,TRMITETT-G,,-1,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,,0,1,0 ;0TRMTMH2-G=M_Nod Termite harvester attack,TRMITETT-G,,-1,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,,0,1,0 ;0TRMTMH3-G=M_Nod Termite harvester attack,TRMITETT-G,,-1,0,EMRAHARV,0200000003000000000000000000000000000000000000000000000000000000,80.000000,40.000000,100.000000,1,0,2,0,,0,1,0 ;0TRMTFM1-G=M_Nod Termite Conyard Attack,TRMITM01-G,,-1,0,GFACT,0100000003000000000000000000000000000000000000000000000000000000,150.000000,40.000000,160.000000,1,0,2,0,,0,1,0 ;0TRMTFM2-G=M_Nod Termite Conyard Attack,TRMITM02-G,,-1,0,SFACT,0100000003000000000000000000000000000000000000000000000000000000,150.000000,40.000000,160.000000,1,0,2,0,,0,1,0 ;0TRMTFM3-G=M_Nod Termite Conyard Attack,TRMITM03-G,,-1,0,AFACT,0100000003000000000000000000000000000000000000000000000000000000,150.000000,40.000000,160.000000,1,0,2,0,,0,1,0 ;0TRMTRM1-G=M_Nod Termite Refinery Attack,TRMTRM01-G,,-1,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,130.000000,40.000000,130.000000,1,0,2,0,,0,1,0 ;0TRMTRM2-G=M_Nod Termite Refinery Attack,TRMTRM02-G,,-1,0,RAPROC,0300000003000000000000000000000000000000000000000000000000000000,130.000000,40.000000,130.000000,1,0,2,0,,0,1,0 ;0TRMTEH0-G=E_Nod Termite harvester attack,TRMITEET-G,,-1,0,TDHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,,1,0,0 ;0TRMTEH1-G=E_Nod Termite harvester attack,TRMITEET-G,,-1,0,FUHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,,1,0,0 ;0TRMTEH2-G=E_Nod Termite harvester attack,TRMITEET-G,,-1,0,RAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,,1,0,0 ;0TRMTEH3-G=E_Nod Termite harvester attack,TRMITEET-G,,-1,0,EMRAHARV,0200000003000000000000000000000000000000000000000000000000000000,60.000000,30.000000,70.000000,1,0,2,0,,1,0,0 ;0TRMTRE1-G=E_Nod Termite Refinery Attack,TRMTRE01-G,,-1,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,80.000000,20.000000,110.000000,1,0,2,0,,0,1,0 ;0TRMTRE2-G=E_Nod Termite Refinery Attack,TRMTRE01-G,,-1,0,RAPROC,0200000003000000000000000000000000000000000000000000000000000000,80.000000,20.000000,110.000000,1,0,2,0,,0,1,0 01A10CND-G=H_GDI A10 GDI Conyard Rush,8A10CNRD-G,,-1,0,GFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,,0,0,1 02A10CND-G=H_GDI A10 Nod Conyard Rush,8A10CNRN-G,,-1,0,NFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,,0,0,1 03A10CND-G=H_GDI A10 Soviet Conyard Rush,8A10CNRS-G,,-1,0,SFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,,0,0,1 04A10CND-G=H_GDI A10 Allied Conyard Rush,8A10CNRA-G,,-1,0,AFACT,0100000003000000000000000000000000000000000000000000000000000000,800.000000,10.000000,1000.000000,1,0,1,0,,0,0,1 01BMTH0H-G=H_Soviet Behemoth Attack,BMTHTTH1-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,120.000000,20.000000,140.000000,1,0,4,0,,0,0,1 ;02BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,,-1,0,ARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;03BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,,-1,0,RAARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;04BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,,-1,0,AIARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;05BMTH0H-G=H_Soviet Behemoth Artillery Counter,BMTHTTH1-G,,-1,0,AIRAARTY,0600000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;06BMTH0H-G=H_Soviet Behemoth V2 Counter,BMTHTTH1-G,,-1,0,V2RL,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;07BMTH0H-G=H_Soviet Behemoth V2 Counter,BMTHTTH1-G,,-1,0,AIV2RL,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;08BMTH0H-G=H_Soviet Behemoth Missile Launcher Counter,BMTHTTH1-G,,-1,0,MSAM,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;09BMTH0H-G=H_Soviet Behemoth Missile Launcher Counter,BMTHTTH1-G,,-1,0,AIMSAM,0500000003000000000000000000000000000000000000000000000000000000,140.000000,80.000000,150.000000,1,0,4,0,,0,0,1 ;01BMTH0M-G=M_Soviet Behemoth Attack,BMTHTTM1-G,,-1,1,RASTEK,0100000003000000000000000000000000000000000000000000000000000000,100.000000,10.000000,130.000000,1,0,4,0,,0,1,0 ;02BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,,-1,0,ARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;03BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,,-1,0,RAARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;04BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,,-1,0,AIARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;05BMTH0M-G=M_Soviet Behemoth Artillery Counter,BMTHTTM1-G,,-1,0,AIRAARTY,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;06BMTH0M-G=M_Soviet Behemoth V2 Counter,BMTHTTM1-G,,-1,0,V2RL,0400000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;07BMTH0M-G=M_Soviet Behemoth V2 Counter,BMTHTTM1-G,,-1,0,AIV2RL,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;08BMTH0H-G=M_Soviet Behemoth Missile Launcher Counter,BMTHTTM1-G,,-1,0,MSAM,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;09BMTH0H-G=M_Soviet Behemoth Missile Launcher Counter,BMTHTTM1-G,,-1,0,AIMSAM,0500000003000000000000000000000000000000000000000000000000000000,120.000000,80.000000,130.000000,1,0,4,0,,0,1,0 ;01BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,,-1,0,MISS,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,,1,0,0 ;02BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,,-1,0,TMPL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,,1,0,0 ;03BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,,-1,0,RAATEK,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,,1,0,0 ;04BMTH0E-G=E_Soviet Behemoth Attack,BMTHTTE1-G,,-1,0,RASTEK,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,4,0,,1,0,0 ; fixed Classic AI triggers 0859C720-G=E_GDI vehicle attack 3,080CF520-G,,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF520-G,1,0,0 0859C3E0-G=E_GDI vehicle attack 7,080D0D90-G,,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D0D90-G,1,0,0 09103C20-G=E_GDI hover pool,090EE280-G,,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE280-G,1,0,0 080D3520-G=M_GDI vehicle attack 7,07EA22FM-G,,-1,1,WEAP_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D6520-G=M_Nod upgrade center attack 2,084E023M-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CE1CM-G,0,1,0 09101B20-G=E_Nod ranged pool,090ED230-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090ED230-G,1,0,0 080D5220-G=M_Nod missile silo attack 2,084DF7EM-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CC92M-G,0,1,0 09F2EA20-G=M_Nod base MLRS vehicle pool,07E7F0EM-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 085940B0-G=H_Nod base MLRS vehicle pool,07E7F0E0-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,,0,0,1 0859BB30-G=H_Nod missile silo attack 2,084DF7E0-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CC920-G,0,0,1 090EFC90-G=H_Nod ranged pool,0832E300-G,,-1,1,AFLD_AI,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E300-G,0,0,1 ; adjusted Classic GDI air attack triggers 0859E270-G=E_GDI aerial base attack 1,07F2A9F0-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07F2A9F0-G,1,0,0 0859E170-G=E_GDI aerial base attack 2,07F2A920-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07EF88B0-G,1,0,0 0859E070-G=E_GDI aerial vehicle attack,07F2A850-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07F2A850-G,1,0,0 085A0320-G=E_GDI ORCA fighter pool,084E4610-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,1,0,0 080CF220-G=M_GDI aerial base attack 1,0B7D4E7M-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,1,0,,0,1,0 080CF120-G=M_GDI aerial base attack 2,0B7D4A9M-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,20.000000,40.000000,1,0,1,0,,0,1,0 0859F140-G=M_GDI ORCA fighter pool,0B7D458M-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 084EFC20-G=H_GDI aerial base attack 2,0B7D4A90-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,55.000000,35.000000,65.000000,1,0,1,0,,0,0,1 084EF920-G=H_GDI ORCA fighter pool,0B7D4580-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,,0,0,1 084EF820-G=H_GDI ORCA bomber pool,0B7D44A0-G,,-1,-1,,0000000000000000000000000000000000000000000000000000000000000000,80.000000,80.000000,80.000000,1,0,1,1,,0,0,1 ================================================ FILE: DXMainClient/Resources/INI/Map Code/Scavenger.ini ================================================ [CrateRules] CrateMinimum=100 [Powerups] Armor=33,ARMOR,0.75 Explosion=0,RAPID,50 Firepower=28,FIREPOWR,1.5 HealBase=0,HEALALL ICBM=10,CHEMISLE Money=28,MONEY,200 Napalm=0,,500 Speed=30,SPEED,1.35 Unit=35, Invulnerability=15,AREAHEAL,1.0 Veteran=0,VETERAN,1 Gas=0,,100 Tiberium=0, Pod=0,MTRINIT [General] PrerequisitePower=BUILDCONST,PYLE_AI,HAND_AI,RATENT_AI,RABARR_AI PrerequisiteBarracks=BUILDCONST,PYLE_AI,HAND_AI,RATENT_AI,RABARR_AI GDIPowerPlant=TWR_AI,GUN_AI,RAPBOX_AI,RAFTUR_AI NodRegularPower=TWR_AI,GUN_AI,RAPBOX_AI,RAFTUR_AI NodAdvancedPower=GUN_AI,ATWR_AI,RAFTUR_AI,RAGUN_AI ;BaseUnit=MHQ MultiplayerAICM=2147483640,2147483640,2147483640 [AI] BuildPower=TWR_AI,GUN_AI,RAGUN_AI,RAFTUR_AI BuildDefense=TWR_AI,GUN_AI,ATWR_AI,RAFTUR_AI,RAGUN_AI BuildAA=GUN_AI,ATWR_AI,RAFTUR_AI,RAGUN_AI PowerSurplus=0 DefenseLimit=10 AARatio=0 AALimit=0 [BUILDCONST] Radar=yes ; ******* Infantry Types ******* [E4S] Prerequisite=RABARR [E5] Prerequisite=HAND [SHOK] Prerequisite=RABARR [ENGINEER] Prerequisite=CABHUT TechLevel=11 [RMBO] TechLevel=11 [CYP] TechLevel=11 [TANYA] TechLevel=11 [VOLKOV] TechLevel=11 ; ******* Vehicle Types ******* [JEEP] Prerequisite=CABHUT Owner=GDI CrateGoodie=yes [RANG] Prerequisite=CABHUT Owner=Allies CrateGoodie=yes [BGGY] Prerequisite=CABHUT Owner=Nod CrateGoodie=yes [APC] Prerequisite=CABHUT Owner=GDI,Nod CrateGoodie=yes [RAAPC] Prerequisite=CABHUT Owner=Allies CrateGoodie=yes [SOVAPC] Prerequisite=CABHUT Owner=Soviet CrateGoodie=yes [BIKE] Prerequisite=CABHUT Owner=Nod CrateGoodie=yes [LTNK] Owner=Nod ;CrateGoodie=yes [1TNK] Prerequisite=CABHUT Owner=Allies CrateGoodie=yes [MTNK] Owner=GDI ;CrateGoodie=yes [2TNK] Prerequisite=AWEAP Cost=800 Owner=Allies ;CrateGoodie=yes [FTNK] Prerequisite=CABHUT Owner=Nod CrateGoodie=yes [3TNK] Owner=Soviet ;CrateGoodie=yes [HTNK] Prerequisite=CABHUT Owner=GDI CrateGoodie=yes [4TNK] Prerequisite=CABHUT Owner=Soviet CrateGoodie=yes [STNK] Prerequisite=CABHUT Owner=Nod CrateGoodie=yes Cloakable=no [HVCT] Prerequisite=CABHUT Owner=Allies CrateGoodie=yes [DTRK] Prerequisite=CABHUT Owner=Soviet CrateGoodie=yes [MWAVE] Prerequisite=CABHUT Owner=Nod CrateGoodie=yes [TTNK] Prerequisite=CABHUT Owner=Soviet CrateGoodie=yes [ARTY] Prerequisite=CABHUT Owner=Nod CrateGoodie=yes [RAARTY] Prerequisite=CABHUT Owner=Allies CrateGoodie=yes [MSAM] Prerequisite=CABHUT Owner=GDI CrateGoodie=yes [MLRS] Prerequisite=CABHUT TechLevel=7 Owner=Nod CrateGoodie=yes [V2RL] Prerequisite=CABHUT Owner=Soviet CrateGoodie=yes [PTNK] Prerequisite=CABHUT Owner=Allies CrateGoodie=yes Cloakable=no [MRV] Prerequisite=CABHUT [MSA] TechLevel=11 [CUMSA] CrateGoodie=no [HARV] CrateGoodie=no [TDHARV] CrateGoodie=no [RAHARV] CrateGoodie=no [FUHARV] CrateGoodie=no [FUTDHARV] CrateGoodie=no [FURAHARV] CrateGoodie=no [GMCV] Prerequisite=WEAP,FIX Owner=GDI Cost=5000 [NMCV] Prerequisite=AFLD,FIX Owner=Nod Cost=5000 [AMCV] Prerequisite=AWEAP,FIX Owner=Allies Cost=5000 [SMCV] Prerequisite=SWEAP,FIX Owner=Soviet Cost=5000 [SAPC] CrateGoodie=no [UTNK] CrateGoodie=no ; ******* Building Types ******* [BRIK] TechLevel=11 [RABRIK] TechLevel=11 [LSRPOST] TechLevel=11 [GGATE_A] TechLevel=11 [GGATE_B] TechLevel=11 [NUKE] TechLevel=11 [RAPOWR] TechLevel=11 [PYLE] Prerequisite=GFACT Power=0 AIBuildThis=no [HAND] Prerequisite=NFACT Power=0 [RATENT] Prerequisite=AFACT Power=0 [RABARR] Prerequisite=SFACT Power=0 [PYLE_AI] AIBuildThis=yes Power=0 [HAND_AI] Power=0 [RATENT_AI] Power=0 [RABARR_AI] Power=0 [TWR] Prerequisite=GFACT Power=0 [ATWR] Prerequisite=GFACT Power=0 [RAPBOX] Prerequisite=AFACT Power=0 [RAHBOX] Prerequisite=AFACT Power=0 [GUN] Prerequisite=NFACT Power=0 [RAGUN] Prerequisite=AFACT Primary=GunCannon Power=0 [RAFTUR] Prerequisite=SFACT Power=0 [SAM] TechLevel=11 [PROC] TechLevel=11 [RAPROC] TechLevel=11 [WEAP] Prerequisite=GFACT Owner=GDI,Civilian Power=0 [AFLD] Prerequisite=NFACT Owner=Nod,Civilian Power=0 [AWEAP] Prerequisite=AFACT Owner=Allies,Civilian Power=0 [SWEAP] Prerequisite=SFACT Owner=Soviet,Civilian Power=0 [WEAP_AI] Prerequisite=none BuildLimit=0 WeaponsFactory=yes Power=0 AIBuildThis=yes [AFLD_AI] Prerequisite=none BuildLimit=0 WeaponsFactory=yes Power=0 AIBuildThis=yes [AWEAP_AI] Prerequisite=none BuildLimit=0 WeaponsFactory=yes Power=0 [SWEAP_AI] Prerequisite=none BuildLimit=0 WeaponsFactory=yes Power=0 [FIX] Prerequisite=none Owner=GDI,Nod,Allies,Soviet Power=0 [RAFIX] TechLevel=11 Power=0 [GSYRD] Prerequisite=CABHUT TechLevel=11 [NSYRD] Prerequisite=CABHUT TechLevel=11 [ASYRD] Prerequisite=CABHUT TechLevel=11 [RASPEN] Prerequisite=CABHUT TechLevel=11 [GSYRD_AI] Prerequisite=CABHUT TechLevel=11 [NSYRD_AI] Prerequisite=CABHUT TechLevel=11 [ASYRD_AI] Prerequisite=CABHUT TechLevel=11 [RASPEN_AI] Prerequisite=CABHUT TechLevel=11 [GHPAD] TechLevel=11 [NHPAD] TechLevel=11 [AHPAD] TechLevel=11 [RAASTRP] TechLevel=11 [NUKE_AI] AIBuildThis=no [NUK2_AI] AIBuildThis=no [NUK2A_AI] AIBuildThis=no [NUK2C_AI] AIBuildThis=no [RAPOWR_AI] AIBuildThis=no [PROC_AI] AIBuildThis=no [TWR_AI] Prerequisite=PYLE_AI Power=0 [GUN_AI] Prerequisite=HAND_AI Power=0 [RAPBOX_AI] Prerequisite=RATENT_AI Power=0 AIBuildThis=no [RAFTUR_AI] Prerequisite=RABARR_AI Power=0 [RAHBOX_AI] Prerequisite=RATENT_AI Power=0 [RAGUN_AI] Prerequisite=RABARR_AI Primary=GunCannon Power=0 [ATWR_AI] Prerequisite=PYLE_AI Power=0 [RATSLA_AI] TechLevel=11 [TowerMissile] Range=7.2 [TTankZap] Damage=100 Range=8.4 [Tiberiums] 0=Riparius 1=Cruentus 2=Vinifera 3=Aboreus [Riparius] Name=Tiberium Riparius Image=1 Power=40 Value=25 Growth=10000 GrowthPercentage=0 Spread=10000 SpreadPercentage=0 Color=NeonGreen [Cruentus] Name=Gems Image=2 Value=60 Growth=10000 GrowthPercentage=10 Spread=10000 SpreadPercentage=0 Power=19 Color=DarkRed Debris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4 [Vinifera] Name=Tiberium Vinifera Image=3 Value=40 Growth=10000 GrowthPercentage=0 Spread=10000 SpreadPercentage=0 Power=17 Color=NeonBlue Debris=VINCRYS1,VINCRYS2,VINCRYS3,VINCRYS4 [Aboreus] Name=Ore Image=4 Value=25 Growth=10000 GrowthPercentage=10 Spread=10000 SpreadPercentage=0 Power=0 Color=DarkGold Debris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4 ;[Tags] ;ShortGameTimerTag=0,Short Game Timer 1,ShortGameTimer ================================================ FILE: DXMainClient/Resources/INI/Map Code/Survivor.ini ================================================ [Events] StartingUnits1=1,13,0,0 StartingUnits2=1,13,0,0 StartingUnits3=1,13,0,0 StartingUnits4=1,13,0,0 StartingUnits5=1,13,0,0 StartingUnits6=1,13,0,0 StartingUnits7=1,13,0,0 StartingUnits8=1,13,0,0 [General] BaseUnit=MHQ HarvesterUnit=AITDHARV,AIRAHARV,MHQ [CrateRules] CrateMinimum=100 [Powerups] Armor=33,ARMOR,0.75 Explosion=0,RAPID,50 Firepower=28,FIREPOWR,1.5 HealBase=0,HEALALL ICBM=10,CHEMISLE Money=0,MONEY,100 Napalm=0,,500 Speed=30,SPEED,1.35 Unit=75, Invulnerability=15,AREAHEAL,1.0 Veteran=0,VETERAN,1 Gas=0,,100 Tiberium=0, Pod=0,MTRINIT ; ******* Vehicle Types ******* [JEEP] Owner=GDI CrateGoodie=yes [RANG] Owner=Allies CrateGoodie=yes [BGGY] Owner=Nod CrateGoodie=yes [APC] Owner=GDI,Nod CrateGoodie=yes [RAAPC] Owner=Allies CrateGoodie=yes [SOVAPC] Owner=Soviet CrateGoodie=yes [BIKE] Owner=Nod CrateGoodie=yes [LTNK] Owner=Nod CrateGoodie=yes [1TNK] Owner=Allies CrateGoodie=yes [MTNK] Owner=GDI CrateGoodie=yes [2TNK] Cost=800 Owner=Allies CrateGoodie=yes [FTNK] Owner=Nod CrateGoodie=yes [3TNK] Owner=Soviet CrateGoodie=yes [HTNK] Owner=GDI CrateGoodie=yes [4TNK] Owner=Soviet CrateGoodie=yes [STNK] Owner=Nod CrateGoodie=yes Cloakable=no [HVCT] Owner=Allies CrateGoodie=yes [DTRK] Owner=Soviet CrateGoodie=yes [MWAVE] Owner=Nod CrateGoodie=yes [TTNK] Owner=Soviet CrateGoodie=yes [ARTY] Owner=Nod CrateGoodie=yes [RAARTY] Owner=Allies CrateGoodie=yes [MSAM] Owner=GDI CrateGoodie=yes [MLRS] TechLevel=7 Owner=Nod CrateGoodie=yes [V2RL] Owner=Soviet CrateGoodie=yes [PTNK] Owner=Allies CrateGoodie=yes Cloakable=no [MSA] TechLevel=11 [CUMSA] CrateGoodie=no [HARV] CrateGoodie=no [TDHARV] CrateGoodie=no [RAHARV] CrateGoodie=no [FUHARV] CrateGoodie=no [FUTDHARV] CrateGoodie=no [FURAHARV] CrateGoodie=no [SAPC] CrateGoodie=no [UTNK] CrateGoodie=no [HUNTSEEK1] CrateGoodie=no [HUNTSEEK2] CrateGoodie=no [HUNTSEEK3] CrateGoodie=no [HUNTSEEK4] CrateGoodie=no [EMGMCV] Image=MTNKE Name=Medium Tank Category=AFV Primary=90mmDummy Secondary=90mm Strength=400 Armor=heavy TechLevel=3 Turret=yes CrateGoodie=no Sight=5 Speed=6 Owner=GDI Cost=800 Points=8 ROT=5 Explosion=FRAGG1 VoiceSelect=V1AWAIT,V1READY,V1REPORT,V1UNITRP VoiceMove=V1MOVOUT,V1ACKNO,V1AFFIRM,V1RAWAY,V1YESSIR VoiceAttack=V1ACKNO,V1AFFIRM,V1NOPROB,V1RAWAY,V1YESSIR VoiceFeedback= SpeedType=Track AccelerationFactor=0.04 MovementZone=Destroyer ThreatPosed=30 DamageParticleSystems=SparkSys,SmallGreySSys Trainable=yes VeteranAbilities=STRONGER,SIGHT,FASTER EliteAbilities=SELF_HEAL Elite=70mmMsl1 AllowedToStartInMultiplayer=yes [EMNMCV] Image=LTNK Name=Light Tank Category=AFV Prerequisite=AFLD Primary=75mm Strength=350 Armor=heavy TechLevel=3 Turret=yes CrateGoodie=no Sight=5 Speed=6 Owner=Nod Cost=600 Points=6 ROT=13 Explosion=FRAGG1 VoiceSelect=V1AWAIT,V1READY,V1REPORT,V1UNITRP VoiceMove=V1MOVOUT,V1ACKNO,V1AFFIRM,V1RAWAY,V1YESSIR VoiceAttack=V1ACKNO,V1AFFIRM,V1NOPROB,V1RAWAY,V1YESSIR VoiceFeedback= SpeedType=Track AccelerationFactor=0.05 MovementZone=Destroyer ThreatPosed=25 DamageParticleSystems=SparkSys,SmallGreySSys PipScale=Passengers Passengers=1 Trainable=yes VeteranAbilities=STRONGER,SIGHT,FASTER EliteAbilities=SELF_HEAL Elite=75mmE AllowedToStartInMultiplayer=yes [EMAMCV] Image=2TNK Name=Medium Tank Category=AFV Primary=90mmRA Strength=400 Armor=heavy TechLevel=3 Turret=yes CrateGoodie=no Sight=6 Speed=6 Owner=Allies Cost=800 Points=8 ROT=7 Explosion=FRAGG1 VoiceSelect=V5AWAIT,V5REPORT,V5VEHIC,V5YESSIR VoiceMove=V5ACKNO,V5AFFIRM VoiceAttack=V5ACKNO,V5AFFIRM SpeedType=Track AccelerationFactor=0.04 MovementZone=Destroyer ThreatPosed=30 DamageParticleSystems=SparkSys,SmallGreySSys Trainable=yes VeteranAbilities=STRONGER,SIGHT,FASTER EliteAbilities=SELF_HEAL Elite=90mmERA AllowedToStartInMultiplayer=yes [EMSMCV] Image=3TNK Name=Heavy Tank Category=AFV Primary=105mm Strength=400 Armor=heavy TechLevel=3 Turret=yes CrateGoodie=no Sight=6 Speed=5 Owner=Soviet Cost=950 Points=9 ROT=7 Explosion=FRAGG1 VoiceSelect=SV1AWAIT,SV1REPORT,SV1VEHIC,SV1YESSIR VoiceMove=SV1ACKNO,SV1AFFIRM VoiceAttack=SV1ACKNO,SV1AFFIRM SpeedType=Track AccelerationFactor=0.03 MovementZone=Destroyer ThreatPosed=30 DamageParticleSystems=SparkSys,SmallGreySSys Trainable=yes VeteranAbilities=STRONGER,SIGHT,FASTER EliteAbilities=SELF_HEAL Elite=105mmE AllowedToStartInMultiplayer=yes [TTankZap] Damage=100 Range=8.4 [Tiberiums] 0=Riparius 1=Cruentus 2=Vinifera 3=Aboreus [Riparius] Name=Tiberium Riparius Image=1 Power=40 Value=25 Growth=10000 GrowthPercentage=0 Spread=10000 SpreadPercentage=0 Color=NeonGreen [Cruentus] Name=Gems Image=2 Value=60 Growth=10000 GrowthPercentage=10 Spread=10000 SpreadPercentage=0 Power=19 Color=DarkRed Debris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4 [Vinifera] Name=Tiberium Vinifera Image=3 Value=40 Growth=10000 GrowthPercentage=0 Spread=10000 SpreadPercentage=0 Power=17 Color=NeonBlue Debris=VINCRYS1,VINCRYS2,VINCRYS3,VINCRYS4 [Aboreus] Name=Ore Image=4 Value=25 Growth=10000 GrowthPercentage=10 Spread=10000 SpreadPercentage=0 Power=0 Color=DarkGold Debris=CRUCRYS1,CRUCRYS2,CRUCRYS3,CRUCRYS4 ================================================ FILE: DXMainClient/Resources/INI/MapSel.ini ================================================ ;**************************************************************************** ; ; FILE ; MapSel.ini ; ; DESCRIPTION ; This is a scenario progression control file. ; ; AUTHOR ; Denzil E. Long, Jr. ; $Author: $ ; ; DATE ; November 11, 1998 ; $Modtime: $ ; $Revision: $ ; ;**************************************************************************** ; GDI Progression Stages [GDI] Anims=Anims Sounds=GDISFX 1=GDI01 ;1A 2=GDI02 ;2A 3=GDI03 ;3A1 4=GDI04 ;3A2 5=GDI05 ;3B 6=GDI06 ;4A1 7=GDI07 ;4A2 8=GDI08 ;5A1 9=GDI09 ;5A2 10=GDI10 ;5C 11=GDI11 ;5C 12=GDI12 ;5B 13=GDI13 ;5B 14=GDI14 ;6A 15=GDI15 ;6A 16=GDI16 ;6A 17=GDI17 ;6A 18=GDI18 ;6B 19=GDI19 ;7A 20=GDI20 ;8A -> 9A / 9B / 9D 21=GDI21 ;9A -> 9B / 9D 22=GDI22 ;9B -> 9D 23=GDI23 ;9B -> 9C / 9D 24=GDI24 ;9C 25=GDI25 ;9D 26=GDI26 ;10A 27=GDI27 ;10B 28=GDI28 ;11A 29=GDI29 ;11A 30=GDI30 ;12A ; Sound effect entries ; Event = Filename, Volume percentage [GDISFX] Overlay=GSWEEP.AUD,60 TargetFlyIn=BESTBOX.AUD,75 ;MouseOnMap=MOUSEON.AUD,50 ;MouseOffMap=MOUSEOFF.AUD,100 EnterRegion=EFFICIEN.AUD,40 ExitRegion= ClickRegion= ; NOD Progression Stages [Nod] Anims=Anims Sounds=NODSFX 1=NOD01 ;1A - Start - Leads to 2A 2=NOD02 ;2A - From 1A - Leads to 3A1 or 3B 3=NOD03 ;3A2 - From 2A - Leads to 4A1 or 4B2 4=NOD04 ;3A1 - From 3B - Leads to 4A2 or 4B1 5=NOD05 ;3B - From 2A - Leads to 3A1 6=NOD06 ;4A2 - From 3A2 - Leads to 5A 7=NOD07 ;4A1 - From 3A1 - Leads to 5A 8=NOD08 ;4A4 - From 4B2 - Leads to 5A 9=NOD09 ;4A3 - From 4B1 - Leads to 5A 10=NOD10 ;4B2 - From 3A2 - Leads to 4A4 11=NOD11 ;4B1 - From 3A1 - Leads to 4A3 12=NOD12 ;5A - From 4A4 - Leads to 6B or 6C 13=NOD13 ;6B - From 5A - Leads to 6A1 14=NOD14 ;6C - From 5A - Leads to 6A2 15=NOD15 ;6A1 - From 6B - Leads to 7A1 or 7B1 16=NOD16 ;6A2 - From 6C - Leads to 7A2 or 7B2 17=NOD17 ;7A1 - From 6A1 - Leads to 8A 18=NOD18 ;7A2 - From 6A2 - Leads to 8A 19=NOD19 ;7A3 - From 7B1 - Leads to 8A 20=NOD20 ;7A4 - From 7B2 - Leads to 8A 21=NOD21 ;7B1 - From 6A1 - Leads to 7A3 22=NOD22 ;7B2 - From 6A2 - Leads to 7A4 23=NOD23 ;8A - From 7Ax - Leads to 9A or 9B 24=NOD24 ;9A - From 8A - Leads to 10A1 25=NOD25 ;9B - From 8A - Leads to 10A2 26=NOD26 ;10A1 - From 9A - Leads to 11A1 27=NOD27 ;10A2 - From 9B - Leads to 11A2 28=NOD28 ;11A1 - From 10A1 - Leads to 12A1 or 12B1 29=NOD29 ;11A2 - From 10A2 - Leads to 12A2 or 12B2 30=NOD30 ;12B1 - From 11A1 - Leads to 12A 31=NOD31 ;12B2 - From 11A2 - Leads to 12A 32=NOD32 ;12A - From 11A or 12B - Finish [NODSFX] Overlay = NSWEEP.AUD, 60 TargetFlyIn = BESTBOX.AUD, 75 ;MouseOnMap = MOUSEON.AUD, 50 ;MouseOffMap = MOUSEOFF.AUD, 100 EnterRegion = EFFICIEN.AUD, 40 ExitRegion = ClickRegion = ;**************************************************************************** ; Animations ; ; Format: Name, X, Y, Rate ;**************************************************************************** [Anims] TextRect=92,322,332,78 Palette=MapSel.pal 1=SMLOGO.SHP,16,322,5 2=GLOBE.SHP,545,168,5 3=COMPASS.SHP,448,255,5 ;**************************************************************************** ; PROGRESSION FIELDS ; ; Scenario - Name of scenario to play for this stage ; ; Description - Text to display when mouse moves onto clickable region ; ; Text1...n - Text to display (Format: X,Y,Time,String) ; X,Y - Display coordinate ; Time - Time to display text, represented in ticks ; (1/60th second) from start of presentation ; String - String to display ; ; MapVQ - The map VQA to play ; ; Overlays - Overlays that fade up over the last frame of the MapVQ movie ; ; ClickMap - A 256 color PCX file (same resolution as the MapVQ) that ; identifies clickable regions. Each clickable region is ; identified by a unique color ranging from 1 - 255, color 0 ; is considered background and is ignored. The numbered entries ; reflect the stage represented by the color in the clickmap. ; ; Targets - Fly-in target positioning. Format: n,x,y,x,y... where 'n' is ; the number of targets. ;**************************************************************************** ; Leads to 1A [GDI00] Scenario= Description= VoiceOver= MapVQ=GDIMAP01.VQA Overlays=RG01A.SHP,RN01A.SHP Targets=1,144,70 ClickMap=GDICLK01.PCX 1=GDI01 ;1A ; 1A - Leads to 2A [GDI01] Scenario=Maps\Missions\GDI1A.MAP Description=768 VoiceOver=GDI-01.AUD MapVQ=GDIMAP01.VQA Overlays=RG02A.SHP,RN02A.SHP Targets=1,180,80 ClickMap=GDICLK01.PCX 2=GDI02 ;2A ;2A - Leads to 3A2 or 3B [GDI02] Scenario=Maps\Missions\GDI2A.MAP Description=769 VoiceOver=GDI-02.AUD MapVQ=GDIMAP01.VQA Overlays=RG03AB.SHP,RN03AB.SHP Targets=2,290,88,218,108 ClickMap=GDICLK01.PCX 3=GDI04 ;3A2 4=GDI05 ;3B ;3A1 - Leads to 4A1 [GDI03] Scenario=Maps\Missions\GDI3A.MAP Description=770 VoiceOver=GDI-03A.AUD MapVQ=GDIMAP01.VQA Overlays=RG04A1.SHP,RN04A1.SHP Targets=1,360,78 ClickMap=GDICLK01.PCX 5=GDI06 ;4A1 ;3A2 - Leads to 4A2 [GDI04] Scenario=Maps\Missions\GDI3A.MAP Description=770 VoiceOver=GDI-03A.AUD MapVQ=GDIMAP01.VQA Overlays=RG04A2.SHP,RN04A2.SHP Targets=1,360,78 ClickMap=GDICLK01.PCX 5=GDI07 ;4A2 ;3B - Leads to 3A1 [GDI05] Scenario=Maps\Missions\GDI3B.MAP Description=771 VoiceOver=GDI-03B.AUD MapVQ=GDIMAP01.VQA Overlays=RG03A.SHP,RN03A.SHP Targets=1,290,88 ClickMap=GDICLK01.PCX 3=GDI03 ;3A1 ;4A1 - Leads to 5A1 or 5B1 [GDI06] Scenario=Maps\Missions\GDI4A.MAP Description=772 VoiceOver=GDI-04.AUD MapVQ=GDIMAP02.VQA Overlays=RG05AB1.SHP,RN05AB1.SHP Targets=2,188,183,280,256 ClickMap=GDICLK02.PCX 6=GDI08 ;5A1 7=GDI10 ;5C1 ;4A2 - Leads to 5A2 or 5B2 [GDI07] Scenario=Maps\Missions\GDI4A.MAP Description=772 VoiceOver=GDI-04.AUD MapVQ=GDIMAP02.VQA Overlays=RG05AB2.SHP,RN05AB2.SHP Targets=2,188,183,280,256 ClickMap=GDICLK02.PCX 6=GDI09 ;5A2 7=GDI11 ;5C2 ;5A1 - Leads to 5B1 [GDI08] Scenario=Maps\Missions\GDI5A.MAP Description=773 VoiceOver=GDI-05A.AUD MapVQ=GDIMAP02.VQA Overlays=RG05B1.SHP,RN05B1.SHP Targets=1,280,256 ClickMap=GDICLK02.PCX 7=GDI12 [GDI09] Scenario=Maps\Missions\GDI5A.MAP Description=773 VoiceOver=GDI-05A.AUD MapVQ=GDIMAP02.VQA Overlays=RG05B2.SHP,RN05B2.SHP Targets=1,280,256 ClickMap=GDICLK02.PCX 7=GDI13 ;5B1 - Leads to [GDI10] Scenario=Maps\Missions\GDI5C.MAP Description=774 VoiceOver=GDI-05B.AUD MapVQ=GDIMAP03.VQA Overlays=RG06AB2.SHP,RN06AB2.SHP Targets=2,218,192,300,230 ClickMap=GDICLK03.PCX 8=GDI16 9=GDI18 [GDI11] Scenario=Maps\Missions\GDI5C.MAP Description=774 VoiceOver=GDI-05B.AUD MapVQ=GDIMAP03.VQA Overlays=RG06AB4.SHP,RN06AB4.SHP Targets=2,218,192,300,230 ClickMap=GDICLK03.PCX 8=GDI17 9=GDI18 [GDI12] Scenario=Maps\Missions\GDI5B.MAP Description=774 VoiceOver=GDI-05B.AUD MapVQ=GDIMAP03.VQA Overlays=RG06AB1.SHP,RN06AB1.SHP Targets=2,218,192,300,230 ClickMap=GDICLK03.PCX 8=GDI14 9=GDI18 [GDI13] Scenario=Maps\Missions\GDI5B.MAP Description=774 VoiceOver=GDI-05B.AUD MapVQ=GDIMAP03.VQA Overlays=RG06AB3.SHP,RN06AB3.SHP Targets=2,218,192,300,230 ClickMap=GDICLK03.PCX 8=GDI15 9=GDI18 [GDI14] Scenario=Maps\Missions\GDI6A.MAP Description=775 VoiceOver=GDI-06A.AUD MapVQ=GDIMAP03.VQA Overlays=RG06B1.SHP,RN06B1.SHP Targets=1,300,230 ClickMap=GDICLK03.PCX 9=GDI18 [GDI15] Scenario=Maps\Missions\GDI6A.MAP Description=775 VoiceOver=GDI-06A.AUD MapVQ=GDIMAP03.VQA Overlays=RG06B3.SHP,RN06B3.SHP Targets=1,300,230 ClickMap=GDICLK03.PCX 9=GDI18 [GDI16] Scenario=Maps\Missions\GDI6A.MAP Description=775 VoiceOver=GDI-06A.AUD MapVQ=GDIMAP03.VQA Overlays=RG06B2.SHP,RN06B2.SHP Targets=1,300,230 ClickMap=GDICLK03.PCX 9=GDI18 [GDI17] Scenario=Maps\Missions\GDI6A.MAP Description=775 VoiceOver=GDI-06A.AUD MapVQ=GDIMAP03.VQA Overlays=RG06B4.SHP,RN06B4.SHP Targets=1,300,230 ClickMap=GDICLK03.PCX 9=GDI18 [GDI18] Scenario=Maps\Missions\GDI6B.MAP Description=776 VoiceOver=GDI-06B.AUD MapVQ=GDIMAP04.VQA Overlays=RG07A.SHP,RN07A.SHP Targets=1,272,32 ClickMap=GDICLK04.PCX 10=GDI19 [GDI19] Scenario=Maps\Missions\GDI7A.MAP Description=777 VoiceOver=GDI-07.AUD MapVQ=GDIMAP04.VQA Overlays=RG08A.SHP,RN08A.SHP Targets=1,168,154 ClickMap=GDICLK04.PCX 11=GDI20 [GDI20] Scenario=Maps\Missions\GDI8A.MAP Description=778 VoiceOver=GDI-08.AUD MapVQ=GDIMAP04.VQA Overlays=RG09ABD.SHP,RN09ABD.SHP Targets=3,116,274,82,190,64,242 ClickMap=GDICLK04.PCX 12=GDI21 ;9A 13=GDI22 ;9B -> 9D 15=GDI25 ;9D ;9A [GDI21] Scenario=Maps\Missions\GDI9A.MAP Description=779 VoiceOver=GDI-09A.AUD MapVQ=GDIMAP04.VQA Overlays=RG09BD.SHP,RN09BD.SHP Targets=2,82,190,64,242 ClickMap=GDICLK04.PCX 13=GDI23 ;9B 15=GDI25 ;9D ;9B -> 9D [GDI22] Scenario=Maps\Missions\GDI9B.MAP Description=780 VoiceOver=GDI-09B.AUD MapVQ=GDIMAP04.VQA Overlays=RG09D1.SHP,RN09D1.SHP Targets=1,64,242 ClickMap=GDICLK04.PCX 15=GDI25 ;9D ;9B -> 9C / 9D [GDI23] Scenario=Maps\Missions\GDI9B.MAP Description=780 VoiceOver=GDI-09B.AUD MapVQ=GDIMAP04.VQA Overlays=RG09CD.SHP,RN09CD.SHP Targets=2,110,228,64,242 ClickMap=GDICLK04.PCX 14=GDI24 ;9C 15=GDI25 ;9D ;9C [GDI24] Scenario=Maps\Missions\GDI9C.MAP Description=781 VoiceOver=GDI-09C.AUD MapVQ=GDIMAP04.VQA Overlays=RG09D2.SHP,RN09D2.SHP Targets=1,64,242 ClickMap=GDICLK04.PCX 15=GDI25 ;9D ;9D [GDI25] Scenario=Maps\Missions\GDI9D.MAP Description=782 VoiceOver=GDI-09D.AUD MapVQ=GDIMAP05.VQA Overlays=RG10AB1.SHP,RN10AB1.SHP Targets=2,160,72,206,30 ClickMap=GDICLK05.PCX 16=GDI26 ;10A 17=GDI27 ;10B [GDI26] Scenario=Maps\Missions\GDI10A.MAP Description=783 VoiceOver=GDI-10A.AUD MapVQ=GDIMAP05.VQA Overlays=RG11A1.SHP,RN11A1.SHP Targets=1,230,148 ClickMap=GDICLK05.PCX 18=GDI28 [GDI27] Scenario=Maps\Missions\GDI10B.MAP Description=784 VoiceOver=GDI-10B.AUD MapVQ=GDIMAP05.VQA Overlays=RG11A2.SHP,RN11A2.SHP Targets=1,230,148 ClickMap=GDICLK05.PCX 18=GDI29 [GDI28] Scenario=Maps\Missions\GDI11A.MAP Description=785 VoiceOver=GDI-11.AUD MapVQ=GDIMAP05.VQA Overlays=RG12A1.SHP,RN12A1.SHP Targets=1,282,252 ClickMap=GDICLK05.PCX 19=GDI30 [GDI29] Scenario=Maps\Missions\GDI11A.MAP Description=785 VoiceOver=GDI-11.AUD MapVQ=GDIMAP05.VQA Overlays=RG12A2.SHP,RN12A2.SHP Targets=1,282,252 ClickMap=GDICLK05.PCX 19=GDI30 [GDI30] Scenario=Maps\Missions\GDI12A.MAP Description=786 VoiceOver=GDI-12.AUD ;**************************************************************************** ; NOD STAGES ;**************************************************************************** ;Leads to 1A [NOD00] Scenario=Maps\Missions\ Description= VoiceOver= MapVQ=NODMAP01.VQA Overlays=TN01A.SHP,TG01A.SHP Targets=1,120,140 ClickMap=NODCLK01.PCX 1=NOD01 ;1A ;1A - Leads to 2A [NOD01] Scenario=Maps\Missions\NOD1A.MAP Description=787 VoiceOver=NOD-01.AUD MapVQ=NODMAP01.VQA Overlays=TN02A.SHP,TG02A.SHP Targets=1,190,100 ClickMap=NODCLK01.PCX 2=NOD02 ;2A ;2A - Leads to 3A2 or 3B [NOD02] Scenario=Maps\Missions\NOD2A.MAP Description=788 VoiceOver=NOD-02.AUD MapVQ=NODMAP01.VQA Overlays=TN03AB.SHP,TG03AB.SHP Targets=2,388,168,300,204 ClickMap=NODCLK01.PCX 3=NOD03 ;3A2 4=NOD05 ;3B ;3A2 - Leads to 4A2 or 4B2 [NOD03] Scenario=Maps\Missions\NOD3A.MAP Description=789 VoiceOver=NOD-03A.AUD MapVQ=NODMAP02.VQA Overlays=TN04AB2.SHP,TG04AB2.SHP Targets=2,244,40,272,96 ClickMap=NODCLK02.PCX 5=NOD06 ;4A2 6=NOD10 ;4B2 ;3A1 - Leads to 4A1 or 4B1 [NOD04] Scenario=Maps\Missions\NOD3A.MAP Description=789 VoiceOver=NOD-03A.AUD MapVQ=NODMAP02.VQA Overlays=TN04AB1.SHP,TG04AB1.SHP Targets=2,244,40,272,96 ClickMap=NODCLK02.PCX 5=NOD07 ;4A1 6=NOD11 ;4B1 ;3B - Leads to 3A1 [NOD05] Scenario=Maps\Missions\NOD3B.MAP Description=790 VoiceOver=NOD-03B.AUD MapVQ=NODMAP01.VQA Overlays=TN03A.SHP,TG03A.SHP Targets=1,388,168 ClickMap=NODCLK01.PCX 3=NOD04 ;3A1 ;4A2 - Leads to 5A [NOD06] Scenario=Maps\Missions\NOD4A.MAP Description=791 VoiceOver=NOD-04A.AUD MapVQ=NODMAP03.VQA Overlays=TN05A.SHP,TG05A.SHP Targets=1,206,138 ClickMap=NODCLK03.PCX 7=NOD12 ;5A ;4A1 - Leads to 5A [NOD07] Scenario=Maps\Missions\NOD4A.MAP Description=791 VoiceOver=NOD-04A.AUD MapVQ=NODMAP03.VQA Overlays=TN05A.SHP,TG05A.SHP Targets=1,206,138 ClickMap=NODCLK03.PCX 7=NOD12 ;5A ;4A4 - Leads to 5A [NOD08] Scenario=Maps\Missions\NOD4A.MAP Description=791 VoiceOver=NOD-04A.AUD MapVQ=NODMAP03.VQA Overlays=TN05A.SHP,TG05A.SHP Targets=1,206,138 ClickMap=NODCLK03.PCX 7=NOD12 ;5A ;4A3 - Leads to 5A [NOD09] Scenario=Maps\Missions\NOD4A.MAP Description=791 VoiceOver=NOD-04A.AUD MapVQ=NODMAP03.VQA Overlays=TN05A.SHP,TG05A.SHP Targets=1,206,138 ClickMap=NODCLK03.PCX 7=NOD12 ;5A ;4B2 - Leads to 4A4 [NOD10] Scenario=Maps\Missions\NOD4B.MAP Description=854 VoiceOver=NOD-04B.AUD MapVQ=NODMAP02.VQA Overlays=TN04A2.SHP,TG04A2.SHP Targets=1,244,40 ClickMap=NODCLK02.PCX 5=NOD08 ;4A4 ;4B1 - Leads to 4A3 [NOD11] Scenario=Maps\Missions\NOD4B.MAP Description=854 VoiceOver=NOD-04B.AUD MapVQ=NODMAP02.VQA Overlays=TN04A1.SHP,TG04A1.SHP Targets=1,244,40 ClickMap=NODCLK02.PCX 5=NOD09 ;4A3 ;5A - Leads to 6B or 6C [NOD12] Scenario=Maps\Missions\NOD5A.MAP Description=792 VoiceOver=NOD-05.AUD MapVQ=NODMAP03.VQA Overlays=TN06BC.SHP,TG06BC.SHP Targets=2,148,34,122,128 ClickMap=NODCLK03.PCX 9=NOD13 ;6B 10=NOD14 ;6C ;6B - Leads to 6A1 [NOD13] Scenario=Maps\Missions\NOD6B.MAP Description=794 VoiceOver=NOD-06B.AUD MapVQ=NODMAP03.VQA Overlays=TN06A1.SHP,TG06A1.SHP Targets=1,72,66 ClickMap=NODCLK03.PCX 8=NOD15 ;6A1 ;6C - Leads to 6A2 [NOD14] Scenario=Maps\Missions\NOD6C.MAP Description=795 VoiceOver=NOD-06C.AUD MapVQ=NODMAP03.VQA Overlays=TN06A2.SHP,TG06A2.SHP Targets=1,72,66 ClickMap=NODCLK03.PCX 8=NOD16 ;6A2 ;6A1 - Leads to 7A1 or 7B1 [NOD15] Scenario=Maps\Missions\NOD6A.MAP Description=793 VoiceOver=NOD-06A.AUD MapVQ=NODMAP03.VQA Overlays=TN07AB1.SHP,TG07AB1.SHP Targets=2,328,260,302,170 ClickMap=NODCLK03.PCX 11=NOD17 ;7A1 12=NOD21 ;7B1 ;6A2 - Leads to 7A2 or 7B2 [NOD16] Scenario=Maps\Missions\NOD6A.MAP Description=793 VoiceOver=NOD-06A.AUD MapVQ=NODMAP03.VQA Overlays=TN07AB2.SHP,TG07AB2.SHP Targets=2,328,260,302,170 ClickMap=NODCLK03.PCX 11=NOD18 ;7A2 12=NOD22 ;7B2 ;7A1 - Leads to 8A [NOD17] Scenario=Maps\Missions\NOD7A.MAP Description=796 VoiceOver=NOD-07A.AUD MapVQ=NODMAP04.VQA Overlays=TN08A.SHP,TG08A.SHP Targets=1,316,112 ClickMap=NODCLK04.PCX 13=NOD23 ;8A ;7A2 - Leads to 8A [NOD18] Scenario=Maps\Missions\NOD7A.MAP Description=796 VoiceOver=NOD-07A.AUD MapVQ=NODMAP04.VQA Overlays=TN08A.SHP,TG08A.SHP Targets=1,316,112 ClickMap=NODCLK04.PCX 13=NOD23 ;8A ;7A3 - Leads to 8A [NOD19] Scenario=Maps\Missions\NOD7A.MAP Description=796 VoiceOver=NOD-07A.AUD MapVQ=NODMAP04.VQA Overlays=TN08A.SHP,TG08A.SHP Targets=1,316,112 ClickMap=NODCLK04.PCX 13=NOD23 ;8A ;7A4 - Leads to 8A [NOD20] Scenario=Maps\Missions\NOD7A.MAP Description=796 VoiceOver=NOD-07A.AUD MapVQ=NODMAP04.VQA Overlays=TN08A.SHP,TG08A.SHP Targets=1,316,112 ClickMap=NODCLK04.PCX 13=NOD23 ;8A ;7B1 - Leads to 7A3 [NOD21] Scenario=Maps\Missions\NOD7B.MAP Description=797 VoiceOver=NOD-07B.AUD MapVQ=NODMAP03.VQA Overlays=TN07A1.SHP,TG07A1.SHP Targets=1,328,260 ClickMap=NODCLK03.PCX 11=NOD19 ;7A3 ;7B2 - Leads to 7A4 [NOD22] Scenario=Maps\Missions\NOD7B.MAP Description=797 VoiceOver=NOD-07B.AUD MapVQ=NODMAP03.VQA Overlays=TN07A2.SHP,TG07A2.SHP Targets=1,328,260 ClickMap=NODCLK03.PCX 11=NOD20 ;7A4 ;8A - Leads to 9A or 9B [NOD23] Scenario=Maps\Missions\NOD8A.MAP Description=798 VoiceOver=NOD-08.AUD MapVQ=NODMAP05.VQA Overlays=TN09AB.SHP,TG09AB.SHP Targets=2,202,262,218,212 ClickMap=NODCLK05.PCX 14=NOD24 ;9A 15=NOD25 ;9B ;9A - Leads to 10A1 [NOD24] Scenario=Maps\Missions\NOD9A.MAP Description=799 VoiceOver=NOD-09A.AUD MapVQ=NODMAP05.VQA Overlays=TN10A1.SHP,TG10A1.SHP Targets=1,114,238 ClickMap=NODCLK05.PCX 16=NOD26 ;10A1 ;9B - Leads to 10A2 [NOD25] Scenario=Maps\Missions\NOD9B.MAP Description=800 VoiceOver=NOD-09B.AUD MapVQ=NODMAP05.VQA Overlays=TN10A2.SHP,TG10A2.SHP Targets=1,114,238 ClickMap=NODCLK05.PCX 16=NOD27 ;10A2 ;10A1 - Leads to 11A1 [NOD26] Scenario=Maps\Missions\NOD10A.MAP Description=801 VoiceOver=NOD-10.AUD MapVQ=NODMAP05.VQA Overlays=TN11A1.SHP,TG11A1.SHP Targets=1,362,96 ClickMap=NODCLK05.PCX 17=NOD28 ;11A1 ;10A2 - Leads to 11A2 [NOD27] Scenario=Maps\Missions\NOD10A.MAP Description=801 VoiceOver=NOD-10.AUD MapVQ=NODMAP05.VQA Overlays=TN11A2.SHP,TG11A2.SHP Targets=1,362,96 ClickMap=NODCLK05.PCX 17=NOD29 ;11A2 ;11A1 - Leads to 12A1 or 12B1 [NOD28] Scenario=Maps\Missions\NOD11A.MAP Description=802 VoiceOver=NOD-11.AUD MapVQ=NODMAP05.VQA Overlays=TN12AB1.SHP,TG12AB1.SHP Targets=2,422,34,472,84 ClickMap=NODCLK05.PCX 18=NOD32 ;12A 19=NOD30 ;12B1 ;11A2 - Leads to 12A2 or 12B2 [NOD29] Scenario=Maps\Missions\NOD11A.MAP Description=802 VoiceOver=NOD-11.AUD MapVQ=NODMAP05.VQA Overlays=TN12AB2.SHP,TG12AB2.SHP Targets=2,422,34,472,84 ClickMap=NODCLK05.PCX 18=NOD32 ;12A 19=NOD31 ;12B2 ;12B1 - Leads to 12A [NOD30] Scenario=Maps\Missions\NOD12B.MAP Description=804 VoiceOver=NOD-12B.AUD MapVQ=NODMAP05.VQA Overlays=TN12A1.SHP,TG12A1.SHP Targets=1,422,34 ClickMap=NODCLK05.PCX 18=NOD32 ;12A ;12B2 - Leads to 12A [NOD31] Scenario=Maps\Missions\NOD12B.MAP Description=804 VoiceOver=NOD-12B.AUD MapVQ=NODMAP05.VQA Overlays=TN12A2.SHP,TG12A2.SHP Targets=1,422,34 ClickMap=NODCLK05.PCX 18=NOD32 ;12A ;12A - Finish [NOD32] Scenario=Maps\Missions\NOD12A.MAP Description=803 VoiceOver=NOD-12A.AUD ================================================ FILE: DXMainClient/Resources/INI/MapSel01.ini ================================================ ;**************************************************************************** ; ; FILE ; MapSel01.ini ; ; DESCRIPTION ; Scenario progression control file for Firestorm ; ; AUTHOR ; Denzil E. Long, Jr. ; ; DATE ; November 9, 1999 ; ;**************************************************************************** ; GDI Progression Stages [GDI] Anims=Anims Sounds=GDISFX 1=FSGDI01 2=FSGDI02 3=FSGDI03 4=FSGDI04 5=FSGDI05 6=FSGDI06 7=FSGDI07 8=FSGDI08 9=FSGDI09 ; Sound effect entries ; Event = Filename, Volume percentage [GDISFX] TargetFlyIn=BESTBOX.AUD,75 EnterRegion=EFFICIEN.AUD,40 ; NOD Progression Stages [Nod] Anims=Anims Sounds=NODSFX 1=FSNOD01 2=FSNOD02 3=FSNOD03 4=FSNOD04 5=FSNOD05 6=FSNOD06 7=FSNOD07 8=FSNOD08 9=FSNOD09 [NODSFX] TargetFlyIn = BESTBOX.AUD, 75 EnterRegion = EFFICIEN.AUD, 40 ;**************************************************************************** ; Animations ; ; Format: Name, X, Y, Rate ;**************************************************************************** [Anims] TextRect=92,322,332,78 Palette=MapSel.pal 1=SMLOGO.SHP,16,322,5 2=GLOBE.SHP,545,168,5 3=COMPASS.SHP,448,255,5 ;**************************************************************************** ; PROGRESSION FIELDS ; ; Scenario - Name of scenario to play for this stage ; ; Description - Text to display when mouse moves onto clickable region ; ; Text1...n - Text to display (Format: X,Y,Time,String) ; X,Y - Display coordinate ; Time - Time to display text, represented in ticks ; (1/60th second) from start of presentation ; String - String to display ; ; VoiceOver - Audio file to play when mouse enters click region ; ; MapVQ - The map VQA to play ; ; Overlays - Overlays that fade up over the last frame of the MapVQ movie ; ; ClickMap - A 256 color PCX file (same resolution as the MapVQ) that ; identifies clickable regions. Each clickable region is ; identified by a unique color ranging from 1 - 255 (Color 0 ; is considered background and is ignored). The numbered entries ; reflect the stage represented by the color in the clickmap. ; ; Targets - Fly-in target positioning. Format: n,x,y,x,y... where 'n' is ; the number of targets. ;**************************************************************************** ; 1 leads to 2 [FSGDI01] MapVQ=FSGMAP02.VQA Targets=1,314,128 ClickMap=FSGCLK02.PCX 2=FSGDI02 ; 2 leads to 3 [FSGDI02] Scenario=Maps\Missions\FSGDI02.MAP Description=GDIBRIEF02 MapVQ=FSGMAP03.VQA Targets=1,214,128 ClickMap=FSGCLK03.PCX 3=FSGDI03 [GDIBRIEF02] 1=We have lost communication with a small nearby civilian settlement. 2=Their last message spoke of strange monsters attacking them. We do not have 3=time to wait for a larger force and must investigate. Protect the civilians 4=at all costs. Evacuate as many civilians as possible and get them to the 5=pickup zone for immediate air transport. ; 3 leads to 4 [FSGDI03] Scenario=Maps\Missions\FSGDI03.MAP Description=GDIBRIEF03 MapVQ=FSGMAP04.VQA Targets=1,324,156 ClickMap=FSGCLK04.PCX 4=FSGDI04 [GDIBRIEF03] 1=The death of Tratos has caused open revolt among the mutants. For some reason 2=they believe that the local food and water supplies have been poisoned, and 3=are attacking the local depot. This has upset the civilians in the area, 4=causing armed conflict between the two factions. Quell the rioting and prevent 5=needless deaths and damage on BOTH sides of the conflict. To this end we have 6=equipped your infantry with non-lethal weaponry. In addition you must prevent 7=the destruction of the depot, as it supplies all of the relocated civilians and 8=mutants in the area. ; 4 leads to 5 [FSGDI04] Scenario=Maps\Missions\FSGDI04.MAP Description=GDIBRIEF04 MapVQ=FSGMAP05.VQA Targets=1,250,124 ClickMap=FSGCLK05.PCX 5=FSGDI05 [GDIBRIEF04] 1=We believe CABAL's core to be in this area. Neutralize the two bridges to cut 2=off enemy reinforcements. Capture CABAL using an engineer. Nod is using 3=self-powered laser fencing to keep intruders away from the core. There should 4=be command stations that you can capture to disable this fencing. Finally, deal 5=with any remaining defenses guarding that core. ; 5 leads to 6 [FSGDI05] Scenario=Maps\Missions\FSGDI05.MAP Description=GDIBRIEF05 MapVQ=FSGMAP06.VQA Targets=1,330,118 ClickMap=FSGCLK06.PCX 6=FSGDI06 [GDIBRIEF05] 1=The second Tacitus piece is in an ancient temple located outside of La Paz, 2=Bolivia. Locate the temple and retrieve the Tacitus. Be cautious, as the 3=area is completely uncharted. ; 6 leads to 7 [FSGDI06] Scenario=Maps\Missions\FSGDI06.MAP Description=GDIBRIEF06 MapVQ=FSGMAP07.VQA Targets=1,300,138 ClickMap=FSGCLK07.PCX 7=FSGDI07 [GDIBRIEF06] 1=CABAL has betrayed us. GDI and perhaps the Earth itself are doomed unless 2=we can call back and regroup enough to send for help. Our first priority 3=is to get Dr. Boudreau to the relative safety of a nearby GDI outpost. 4=Once she is safe, we can call for reinforcements and hopefully remove at 5=least this part of CABAL's forces. ; 7 leads to 8 [FSGDI07] Scenario=Maps\Missions\FSGDI07.MAP Description=GDIBRIEF07 MapVQ=FSGMAP08.VQA Targets=1,330,104 ClickMap=FSGCLK08.PCX 8=FSGDI08 [GDIBRIEF07] 1=We've lost communication with our base outside of Trondheim. Get in there 2=and find out what's happening. ; 8 leads to 9 [FSGDI08] Scenario=Maps\Missions\FSGDI08.MAP Description=GDIBRIEF08 MapVQ=FSGMAP09.VQA Targets=1,200,126 ClickMap=FSGCLK09.PCX 9=FSGDI09 [GDIBRIEF08] 1=Our scientists have reprogrammed a cyborg given to us by Nod forces. 2=Carried within its internal circuitry is a virus, which it will release 3=into CABAL's communications network. The cyborg must be inserted into the 4=defensive outpost that lies between our forces and the cyborg creation 5=plant. The lives of many civilians are at stake, and CABAL knows we are 6=coming. The longer it takes to establish your base the more heavily he 7=will be defended. GOOD LUCK! ; 9 Ends the game [FSGDI09] Scenario=Maps\Missions\FSGDI09.MAP Description=GDIBRIEF09 [GDIBRIEF09] 1=Take CABAL down fast and hard. No mercy and no surrender. Find a way to 2=get in there and take the core out. ;**************************************************************************** ; NOD STAGES ;**************************************************************************** ; 1 leads to 2 [FSNOD01] MapVQ=FSNMAP02.VQA Targets=1,312,128 ClickMap=FSNCLK02.PCX 2=FSNOD02 ; 2 leads to 3 [FSNOD02] Scenario=Maps\Missions\FSNOD02.MAP Description=NODBRIEF02 MapVQ=FSNMAP03.VQA Targets=1,272,102 ClickMap=FSNCLK03.PCX 3=FSNOD03 [NODBRIEF02] 1=The first step in our Tiberium evolution requires the fertilization of 2=the land with new indigenous life forms. We will use this new life to 3=educate those who wish to interfere with its progress. Establish your 4=base near the Genesis Pit. It is here that you will find the seeds of 5=evolution. Lure the life forms out of their womb and to the feeding 6=grounds. A nearby civilian settlement will serve as bait. ; 3 leads to 4 [FSNOD03] Scenario=Maps\Missions\FSNOD03.MAP Description=NODBRIEF03 MapVQ=FSNMAP04.VQA Targets=1,220,134 ClickMap=FSNCLK04.PCX 4=FSNOD04 [NODBRIEF03] 1=While GDI's forces have been diverted towards defending the civilians, 2=you are to lead an elite strike force in an assassination operation against 3=the leader of the mutants. Locate Tratos within the base. Our new Limpet 4=mines will help to do this. Once located you must devise a way to reach and 5=kill him. GDI will still have considerable protection for Tratos, as he is 6=their last hope at defeating the Tiberium onslaught. We know that he will 7=have mutant guardians, sensor arrays and GDI will have active firestorm walls 8=set-up throughout the base. Destroying their power supply should neutralize 9=the firestorm, an airstrike will deal with the sensor arrays and the rest is 10=up to you. Do not fail as this mission is integral to the future of Nod. ; 4 leads to 5 [FSNOD04] Scenario=Maps\Missions\FSNOD04.MAP Description=NODBRIEF04 MapVQ=FSNMAP05.VQA Targets=1,378,80 ClickMap=FSNCLK05.PCX 5=FSNOD05 [NODBRIEF04] 1=The mutant vermin have once again made themselves known. They have stolen the 2=Tacitus that I...we have worked so hard to obtain. If Kane's work is to be completed, 3=we must recover it. Find the mutant encampment and recover the Tacitus. Once it is 4=safely removed, terminate all mutants in the area. Perhaps this will teach them not 5=to interfere with us again. ; 5 leads to 6 [FSNOD05] Scenario=Maps\Missions\FSNOD05.MAP Description=NODBRIEF05 MapVQ=FSNMAP06.VQA Targets=1,324,66 ClickMap=FSNCLK06.PCX 6=FSNOD06 [NODBRIEF05] 1=CABAL has betrayed us all. We must escape to regroup and repay his treachery. 2=There is an abandoned airfield nearby; if we can reach it, we have a chance. 3=Once we are there, we must repair the array to contact our forces and call for 4=an evac. We have no information or tactical support now that CABAL has gone 5=rogue, so we are on our own. ; 6 leads to 7 [FSNOD06] Scenario=Maps\Missions\FSNOD06.MAP Description=NODBRIEF06 MapVQ=FSNMAP07.VQA Targets=1,276,60 ClickMap=FSNCLK07.PCX 7=FSNOD07 [NODBRIEF06] 1=Since CABAL has turned on us we are suffering a communications blackout. We are 2=forced to try and obtain GDI's EVA technology. There is a small GDI airbase in 3=this sector. Get your engineer into their radar to steal an EVA unit. You may want 4=to consider trying to create a distraction to otherwise preoccupy the GDI air units. 5=Also, we have a new unit for you, the Mobile Stealth Generator. Use it wisely. ; 7 leads to 8 [FSNOD07] Scenario=Maps\Missions\FSNOD07.MAP Description=NODBRIEF07 MapVQ=FSNMAP08.VQA Targets=1,328,124 ClickMap=FSNCLK08.PCX 8=FSNOD08 [NODBRIEF07] 1=Scorched earth, plain and simple. Destroy all cybernetic forces in the area, 2=the base and CABAL's computer core. ; 8 leads to 9 [FSNOD08] Scenario=Maps\Missions\FSNOD08.MAP Description=NODBRIEF08 MapVQ=FSNMAP09.VQA Targets=1,198,130 ClickMap=FSNCLK09.PCX 9=FSNOD09 [NODBRIEF08] 1=Prior to our main assault on CABAL we will need to slow down his production 2=capabilities. CABAL is currently harvesting Tiberium heavily in Eastern Africa. 3=Get in there and eliminate CABAL's harvesting abilities. Unfortunately, at this 4=time we can only afford to provide you with a small strike force, use them wisely. ; 9 Ends the game [FSNOD09] Scenario=Maps\Missions\FSNOD09.MAP Description=NODBRIEF09 [NODBRIEF09] 1=Take CABAL down fast and hard. No mercy and no surrender. Find a way to get in 2=there and take the core out. ================================================ FILE: DXMainClient/Resources/INI/Menu.ini ================================================ [MainMenu] Background=DTABackX ;DTAbacks ;Theme=Intro ;0=ClassicMenuItem ;1=EnhancedMenuItem 2=MainMenuExit 3=Escape ;4=Version ;5=Credits ;6=VersionText ItemMax=6 [ClassicMenuItem] Type=Image ID=100 Image=CDTAC Highlighted=CDTACH HighlightSound=BUTTON.AUD Origin=196,158 ActiveRect=196,158,250,18 [EnhancedMenuItem] Type=Image ID=101 Image=CDTAE Highlighted=CDTAEH HighlightSound=BUTTON.AUD Origin=196,184 ActiveRect=196,184,250,18 [Credits] Type=Image ;Shortcut ID=12 Image=DTA10c Highlighted=DTA10c-h HighlightSound=BUTTON.AUD Origin=196,210 ActiveRect=196,210,121,18 ;Keys=C [MainMenuExit] Type=Image ID=0 Image=DTA9e ;CDTAEX Highlighted=DTA9e-h ;CDTAEXH HighlightSound=BUTTON.AUD Origin=260,195 ;324,210 ActiveRect=260,195,121,18 ;324,210,121,18 [EnhancedMenu] Background=DTABACK Theme=Intro1 0=EnhancedNewCampaign 1=EnhancedLoadMission 2=EnhancedLAN 3=EnhancedInternet ;4=EnhancedSerialModem 5=EnhancedSkirmish 6=EnhancedOptions 7=EnhancedBack 8=EnhancedExit 9=Version 10=EscapeBack 11=Back 12=EnhancedText ItemMax=16 [EnhancedNewCampaign] Type=Image ID=1 Image=DTA1nc Highlighted=DTA1nc-h HighlightSound=BUTTON.AUD Origin=196,118 ActiveRect=196,118,250,18 [EnhancedLoadMission] Type=Image ID=2 Image=DTA2lm Highlighted=DTA2lm-h HighlightSound=BUTTON.AUD Disabled=DTA2lm-g Origin=196,144 ActiveRect=196,144,250,18 [EnhancedLAN] Type=Image ID=3 Image=DTA3l Highlighted=DTA3l-h HighlightSound=BUTTON.AUD Origin=196,170 ActiveRect=196,170,250,18 [EnhancedInternet] Type=Image ID=4 Image=DTA4i Highlighted=DTA4i-h HighlightSound=BUTTON.AUD Origin=196,196 ActiveRect=196,196,250,18 [EnhancedSerialModem] Type=Image ID=5 Image=DTA5sm Highlighted=DTA5sm-h HighlightSound=BUTTON.AUD Origin=196,196 ActiveRect=196,196,250,18 [EnhancedSkirmish] Type=Image ID=6 Image=DTA6s Highlighted=DTA6s-h HighlightSound=BUTTON.AUD Origin=196,222 ActiveRect=196,222,250,18 [EnhancedOptions] Type=Image ID=8 Image=DTA7o Highlighted=DTA7o-h HighlightSound=BUTTON.AUD Origin=196,248 ActiveRect=196,248,250,18 [EnhancedBack] Type=Image ID=102 Image=DTA8b Highlighted=DTA8b-h HighlightSound=BUTTON.AUD Origin=196,274 ActiveRect=196,274,121,18 [EnhancedExit] Type=Image ID=0 Image=DTA9e Highlighted=DTA9e-h HighlightSound=BUTTON.AUD Origin=324,274 ActiveRect=324,274,121,18 [EnhancedText] Type=Image ID=0 Image=DTAEB ;DTAE Origin=226,27 ;226,7 ;293,58 ActiveRect=0,0,0,0 [ClassicMenu] Background=DTABACK Theme=Intro 0=ClassicNewCampaign 1=ClassicLoadMission 2=ClassicLAN 3=ClassicInternet ;4=ClassicSerialModem 5=ClassicSkirmish 6=ClassicOptions 7=ClassicBack 8=ClassicExit 9=Version 10=EscapeBack 11=Back 12=ClassicText ItemMax=15 [ClassicNewCampaign] Type=Image ID=1 Image=DTA1nc Highlighted=DTA1nc-h HighlightSound=BUTTON.AUD Origin=196,118 ActiveRect=196,118,250,18 [ClassicLoadMission] Type=Image ID=2 Image=DTA2lm Highlighted=DTA2lm-h HighlightSound=BUTTON.AUD Disabled=DTA2lm-g Origin=196,144 ActiveRect=196,144,250,18 [ClassicLAN] Type=Image ID=3 Image=DTA3l Highlighted=DTA3l-h HighlightSound=BUTTON.AUD Origin=196,170 ActiveRect=196,170,250,18 [ClassicInternet] Type=Image ID=4 Image=DTA4i Highlighted=DTA4i-h HighlightSound=BUTTON.AUD Origin=196,196 ActiveRect=196,196,250,18 [ClassicSerialModem] Type=Image ID=5 Image=DTA5sm Highlighted=DTA5sm-h HighlightSound=BUTTON.AUD Origin=196,196 ActiveRect=196,196,250,18 [ClassicSkirmish] Type=Image ID=6 Image=DTA6s Highlighted=DTA6s-h HighlightSound=BUTTON.AUD Origin=196,222 ActiveRect=196,222,250,18 [ClassicOptions] Type=Image ID=8 Image=DTA7o Highlighted=DTA7o-h HighlightSound=BUTTON.AUD Origin=196,248 ActiveRect=196,248,250,18 [ClassicBack] Type=Image ID=102 Image=DTA8b Highlighted=DTA8b-h HighlightSound=BUTTON.AUD Origin=196,274 ActiveRect=196,274,121,18 [ClassicExit] Type=Image ID=0 Image=DTA9e Highlighted=DTA9e-h HighlightSound=BUTTON.AUD Origin=324,274 ActiveRect=324,274,121,18 ;[Version] ;Type=Shortcut ;ID=11 ;Keys=ctrl-v [Escape] Type=Shortcut ID=0 Keys=ESC [Back] Type=Shortcut ID=102 Keys=BACKSPACE [EscapeBack] Type=Shortcut ID=102 Keys=ESC [ClassicText] Type=Image ID=0 Image=DTACB ;DTAC Origin=254,26 ;254,6 ;293,58 ActiveRect=0,0,0,0 ;[VersionText] ;Type=Version ;ID=200ss ================================================ FILE: DXMainClient/Resources/INI/ai.ini ================================================ [TaskForces] 0=0832C3F0-G 1=07ECC200-G 2=08063D20-G 3=08063DE0-G 4=0804C6C0-G 5=0804C530-G 6=073A9510-G 7=073A9CC0-G 8=07ECD1F0-G 9=075BFDC0-G 10=0859E2E0-G 11=07EA4290-G 12=073A8CF0-G 13=07ECE4A0-G 14=084B2F60-G 15=07ECFB60-G 16=096473E0-G 17=08603140-G 18=07EA1E90-G 19=08602820-G 20=0860DE90-G 21=07ED0860-G 22=07ED0460-G 23=08050A00-G 24=07ED1330-G 25=073AACA0-G 26=075E02F0-G 27=09F7B380-G 28=073AA3F0-G 29=075E2310-G 30=075E2180-G 31=07E04DC0-G 32=085EC2D0-G 33=09EF0540-G 34=09EF2BB0-G 35=09F11CE0-G 36=08600780-G 37=0965B600-G 38=086001F0-G 39=0965D960-G 40=08600DC0-G 41=08601B60-G 42=086019D0-G 43=08601770-G 44=086015E0-G 45=08601380-G 46=0846A4D0-G 47=08602B40-G 48=086029B0-G 49=08602640-G 50=097246D0-G 51=08602DC0-G 52=086039B0-G 53=0805DAC0-G 54=08060740-G 55=07F3CAE0-G 56=07F3A490-G 57=0860B440-G [0832C3F0-G] Name=1 amphibious APC, 5 engineers 0=5,ENGINEER 1=1,APC Group=-1 [07ECC200-G] Name=1 subterranean APC, 5 engineers 0=1,SAPC 1=5,ENGINEER Group=-1 [08063D20-G] Name=1 subterranean APC, 3 eng, 2 roc 0=1,SAPC 1=3,ENGINEER 2=2,E3 Group=-1 [08063DE0-G] Name=1 amphibious APC,3 eng, 2 disc 0=1,APC 1=3,ENGINEER 2=2,E2 Group=-1 [0804C6C0-G] Name=1 amphibious APC,1 ghost,4 disc 0=1,GHOST 1=1,APC 2=4,E2 Group=-1 [0804C530-G] Name=1 sub. APC, 1 cyb com, 4 cyborgs 0=4,CYBORG 1=1,SAPC 2=1,CYC2 Group=-1 [073A9510-G] Name=4 attack cycles 0=4,BIKE Group=-1 [073A9CC0-G] Name=4 stealth tanks 0=4,STNK Group=-1 [07ECD1F0-G] Name=3 wolverines 0=3,SMECH Group=-1 [075BFDC0-G] Name=3 hover MLRS 0=3,HVR Group=-1 [0859E2E0-G] Name=1 mobile repair vehicle 0=1,REPAIR Group=-1 [07EA4290-G] Name=3 artillery 0=3,ART2 Group=-1 [073A8CF0-G] Name=4 tick tanks 0=4,TTNK Group=-1 [07ECE4A0-G] Name=4 devil's tongue 0=4,SUBTANK Group=-1 [084B2F60-G] Name=1 amphibious APC 0=1,APC Group=-1 [07ECFB60-G] Name=3 titans 0=3,MMCH Group=-1 [096473E0-G] Name=3 disruptors 0=3,SONIC Group=-1 [08603140-G] Name=1 ORCA fighters 0=1,ORCA Group=-1 [07EA1E90-G] Name=1 ORCA bombers 0=1,ORCAB Group=-1 [08602820-G] Name=1 harpies 0=1,APACHE Group=-1 [0860DE90-G] Name=1 banshee 0=1,SCRIN Group=-1 [07ED0860-G] Name=4 titans 0=4,MMCH Group=-1 [07ED0460-G] Name=1 mammoth mk. II 0=1,HMEC Group=-1 [08050A00-G] Name=1 sub APC, 1 hijacker, 4 rocket 0=1,MHIJACK 1=1,SAPC 2=4,E3 Group=-1 [07ED1330-G] Name=4 wolverines 0=4,SMECH Group=-1 [073AACA0-G] Name=4 attack buggies 0=4,BGGY Group=-1 [075E02F0-G] Name=4 disc throwers 0=4,E2 Group=-1 [09F7B380-G] Name=3 jumpjet infantry 0=3,JUMPJET Group=-1 [073AA3F0-G] Name=1 mutant hijacker 0=1,MHIJACK Group=-1 [075E2310-G] Name=4 rocket infantry 0=4,E3 Group=-1 [075E2180-G] Name=4 cyborgs 0=4,CYBORG Group=-1 [07E04DC0-G] Name=4 light infantry 0=4,E1 Group=-1 [085EC2D0-G] Name=1 subterranean APC 0=1,SAPC Group=-1 [09EF0540-G] Name=3 engineers 0=3,ENGINEER Group=-1 [09EF2BB0-G] Name=1 ghoststalker 0=1,GHOST Group=-1 [09F11CE0-G] Name=1 cyborg commando 0=1,CYC2 Group=-1 [08600780-G] Name=1 titans 0=1,MMCH Group=-1 [0965B600-G] Name=1 hover MLRS 0=1,HVR Group=-1 [086001F0-G] Name=1 disruptors 0=1,SONIC Group=-1 [0965D960-G] Name=1 disruptor 0=1,SONIC Group=-1 [08600DC0-G] Name=1 disc throwers 0=1,E2 Group=-1 [08601B60-G] Name=1 jumpjet infantry 0=1,JUMPJET Group=-1 [086019D0-G] Name=1 tick tanks 0=1,TTNK Group=-1 [08601770-G] Name=1 stealth tanks 0=1,STNK Group=-1 [086015E0-G] Name=1 rocket infantry 0=1,E3 Group=-1 [08601380-G] Name=1 cyborgs 0=1,CYBORG Group=-1 [0846A4D0-G] Name=1 attack cycles 0=1,BIKE Group=-1 [08602B40-G] Name=1 devil's tongue 0=1,SUBTANK Group=-1 [086029B0-G] Name=1 engineers 0=1,ENGINEER Group=-1 [08602640-G] Name=1 light infantry 0=1,E1 Group=-1 [097246D0-G] Name=1 artillery 0=1,ART2 Group=-1 [08602DC0-G] Name=1 attack buggies 0=1,BGGY Group=-1 [086039B0-G] Name=1 wolverines 0=1,SMECH Group=-1 [0805DAC0-G] Name=1 amph APC,1 eng, 2 lt. 2 disc 0=1,ENGINEER 1=1,APC 2=2,E2 3=2,E1 Group=-1 [08060740-G] Name=1 sub APC, 1 eng, 2 rocket, 2 lt 0=1,ENGINEER 1=1,SAPC 2=2,E1 3=2,E3 Group=-1 [07F3CAE0-G] Name=1 sub. APC, 3 lt, 2 cyborgs 0=2,CYBORG 1=3,E1 2=1,SAPC Group=-1 [07F3A490-G] Name=1 amphibious APC, 3 lt, 2 disc 0=1,APC 1=2,E2 2=3,E1 Group=-1 [0860B440-G] Name=1 MCV 0=1,MCV Group=-1 [ScriptTypes] 0=085F3E00-G 1=085F3980-G 2=075A3070-G 3=07F7B2A0-G 4=07E686F0-G 5=07F7C5E0-G 6=0786DA60-G 7=07F7D0D0-G 8=07F7E3B0-G 9=0960AAA0-G 10=07F76BE0-G 11=075AD760-G 12=08462780-G 13=075ABE00-G 14=08463030-G 15=088DDE00-G 16=07397BE0-G 17=07F3DE00-G 18=08B50140-G [085F3E00-G] Name=APC/engineer attack 0=14,0 1=43,0 2=47,131084 3=49,0 4=8,2 5=11,14 [085F3980-G] Name=APC/eng. steal money 0=14,0 1=43,0 2=47,131073 3=49,0 4=8,2 5=46,131073 6=46,131074 7=11,14 [075A3070-G] Name=APC/commando attack 0=14,0 1=47,131084 2=49,0 3=8,2 4=0,2 5=0,1 [07F7B2A0-G] Name=Harvester attack 0=0,3 1=0,1 [07E686F0-G] Name=Base defense 0=11,10 [07F7C5E0-G] Name=Base defense attack 0=0,7 1=0,1 [0786DA60-G] Name=Deployed base defense 0=9,0 1=11,10 [07F7D0D0-G] Name=Aerial base attack 0=0,2 1=0,1 [07F7E3B0-G] Name=Vehicle attack 0=0,5 1=0,1 [0960AAA0-G] Name=APC/thief steal vehicles 0=14,0 1=47,1 2=49,0 3=8,2 4=0,5 [07F76BE0-G] Name=Infantry attack 0=0,4 1=0,1 [075AD760-G] Name=Construction yard attack 0=46,131084 1=49,0 2=0,1 [08462780-G] Name=Factories attack 0=0,6 1=0,1 [075ABE00-G] Name=Tiberium refinery attack 0=46,131073 1=49,0 2=0,1 [08463030-G] Name=Power facilities attack 0=0,9 1=0,1 [088DDE00-G] Name=Missile silo attack 0=46,131113 1=49,0 2=0,1 [07397BE0-G] Name=Upgrade center attack 0=46,131076 1=49,0 2=0,1 [07F3DE00-G] Name=APC/infantry attack 0=14,0 1=43,0 2=47,12 3=49,0 4=8,2 5=0,1 [08B50140-G] Name=Replace MCV 0=9,0 [TeamTypes] 0=0832C790-G 1=07EB8E90-G 2=080646D0-G 3=080643E0-G 4=084D4AE0-G 5=084D4730-G 6=0832D7F0-G 7=0832D440-G 8=07E7F400-G 9=0832D120-G 10=07E7F0E0-G 11=07E89580-G 12=0832E770-G 13=07ECE560-G 14=0832E3D0-G 15=0832E300-G 16=0832E230-G 17=0832E160-G 18=090E0AC0-G 19=090E0930-G 20=090E07A0-G 21=090E0620-G 22=084B2AA0-G 23=084B2910-G 24=0B7D4E70-G 25=0B7D4A90-G 26=0B7D48E0-G 27=0B7D4730-G 28=0B7D4580-G 29=0B7D44A0-G 30=0B7D67E0-G 31=0B7D6700-G 32=07ECD3B0-G 33=0753BE90-G 34=07EA0540-G 35=073A8540-G 36=084D7070-G 37=07EA0150-G 38=090E2C20-G 39=07EA1E80-G 40=073A8070-G 41=073A9E50-G 42=073A9D80-G 43=073A9BF0-G 44=0B7F7500-G 45=0B7F7420-G 46=084D8A40-G 47=073A95D0-G 48=073A97A0-G 49=073A93F0-G 50=073A9320-G 51=073A83B0-G 52=07EA2AC0-G 53=07EA2930-G 54=0965C310-G 55=073AA970-G 56=0753EC30-G 57=07EA22F0-G 58=084D9710-G 59=0753E780-G 60=0753E5F0-G 61=073AA2F0-G 62=073AA220-G 63=073AA150-G 64=0753E130-G 65=0753E060-G 66=074A32F0-G 67=073AB9B0-G 68=084DAA80-G 69=090E45F0-G 70=090E4520-G 71=090E4F50-G 72=084DA6A0-G 73=0B7F17D0-G 74=084DA2E0-G 75=084DA210-G 76=084DA140-G 77=084DA070-G 78=0B7F6CA0-G 79=0B7F6BC0-G 80=084DBDB0-G 81=0B7F6A00-G 82=084DBB40-G 83=0B7F6840-G 84=084DB9A0-G 85=084DB8D0-G 86=084DB800-G 87=084DB730-G 88=073AC800-G 89=0B7F4B50-G 90=073AC440-G 91=073ACE80-G 92=073ACDB0-G 93=073AC1B0-G 94=0B7F46F0-G 95=0B7F4610-G 96=09649240-G 97=0B7F4450-G 98=0964EF50-G 99=0B7F8070-G 100=0964EBC0-G 101=0964EAF0-G 102=0964EA20-G 103=0964E950-G 104=07EF9730-G 105=04167990-G 106=07EA6C60-G 107=07EA6B90-G 108=07EF91D0-G 109=04174D70-G 110=075A55C0-G 111=04174BB0-G 112=07E89DD0-G 113=04176E60-G 114=073AEA00-G 115=04173E00-G 116=07E8F140-G 117=07E8CF50-G 118=073AE6C0-G 119=07E8CDB0-G 120=073AE790-G 121=041742A0-G 122=073AEE80-G 123=073AE260-G 124=073AE190-G 125=073AE0C0-G 126=041768B0-G 127=041767D0-G 128=0A9BBF50-G 129=04181EE0-G 130=0A9BBDB0-G 131=04181D20-G 132=0A9BBC10-G 133=0A9BBB40-G 134=0A9BBA70-G 135=0A9BB9A0-G 136=073AF400-G 137=073AF270-G 138=084DE340-G 139=080F71A0-G 140=084DFF50-G 141=084DEA70-G 142=084DE9A0-G 143=084DFBF0-G 144=080CCD80-G 145=080CCCA0-G 146=084DF980-G 147=080CCAE0-G 148=084DF7E0-G 149=080CC920-G 150=084DF640-G 151=084DFE80-G 152=084DFDB0-G 153=084DFCE0-G 154=084DF2C0-G 155=084DF1F0-G 156=084DF120-G 157=084DF050-G 158=084D7830-G 159=084E0F50-G 160=080CEA80-G 161=084E08B0-G 162=084E07E0-G 163=084E0710-G 164=084E0640-G 165=080CE620-G 166=080CE540-G 167=084E03D0-G 168=080CE380-G 169=084E0230-G 170=080CE1C0-G 171=084E0090-G 172=084E0B90-G 173=084E0AC0-G 174=084E09F0-G 175=080CF910-G 176=080CF780-G 177=080CF6B0-G 178=080CF520-G 179=084E1790-G 180=080CF380-G 181=080CFC20-G 182=080C70C0-G 183=080D0D90-G 184=08468050-G 185=07D73AA0-G 186=07D739D0-G 187=07D73840-G 188=07E4EA50-G 189=07E4CF50-G 190=086012B0-G 191=084E1530-G 192=084E1460-G 193=097235F0-G 194=084E21C0-G 195=090EDA60-G 196=090ED990-G 197=084E3E80-G 198=084E3CF0-G 199=090ECA90-G 200=084E3980-G 201=090ED230-G 202=08602F50-G 203=090EDE80-G 204=090EDCF0-G 205=090EDC20-G 206=084E32D0-G 207=090EEB00-G 208=084E3130-G 209=084E3060-G 210=084E4F50-G 211=084E38B0-G 212=084E37E0-G 213=090EEDB0-G 214=090EECE0-G 215=084E4AE0-G 216=084E4A10-G 217=090EE280-G 218=090EE1B0-G 219=084E47A0-G 220=084E4610-G 221=090EE700-G 222=090EE630-G 223=07F2A9F0-G 224=07F2A920-G 225=07F2A850-G 226=084E5E80-G 227=07EF7DF0-G 228=07EF7D20-G 229=084E5B50-G 230=084E5A80-G 231=07EFA790-G 232=07EF88B0-G 233=084E43A0-G 234=084E42D0-G 235=084E55C0-G 236=084E54F0-G 237=084E5420-G 238=084E5350-G 239=084E5280-G 240=084E51B0-G 241=090F0340-G 242=080D32A0-G 243=080D31D0-G 244=080D3100-G 245=080D3030-G 246=080D4A70-G 247=080D49A0-G 248=080D48D0-G 249=080D4800-G 250=07E30830-G 251=07E30760-G 252=080D4590-G 253=080D44C0-G 254=080D43F0-G 255=080D4320-G 256=080D4250-G 257=080D4180-G 258=080D40B0-G 259=084E64C0-G 260=084E63F0-G 261=084E6320-G 262=084E6250-G 263=084E6180-G 264=084E60B0-G 265=084E7F50-G 266=084E7E80-G 267=080D58D0-G 268=080D5800-G 269=080D5730-G 270=080D5660-G 271=080D5590-G 272=080D54C0-G 273=080D53F0-G 274=080D5320-G 275=07ECC9F0-G 276=07ECC920-G 277=07ECC750-G 278=07ECC680-G 279=07ECC4B0-G 280=07ECC3E0-G 281=07ECC210-G 282=07ECC140-G 283=084E70B0-G 284=084D9250-G 285=084DF570-G 286=084DF4A0-G 287=084DF3D0-G 288=084E8F50-G 289=084E8E80-G 290=084E8DB0-G 291=09D00530-G 292=08607590-G 293=086074C0-G 294=084E8A70-G 295=07E8D320-G 296=07E8D250-G 297=084E8740-G 298=084E8670-G 299=084E85A0-G 300=084E84D0-G 301=084E8400-G 302=084E8330-G 303=084E8260-G 304=084E8190-G 305=084E80C0-G 306=084E9F50-G 307=084E9E80-G 308=080D7590-G 309=080D74C0-G 310=080D73F0-G 311=080D7320-G 312=080D7250-G 313=080D7180-G 314=080D70B0-G 315=080C8F50-G 316=086083F0-G 317=09D6EF50-G 318=080CF110-G 319=07FCFC60-G 320=07FCFB90-G 321=07FCFAC0-G 322=08750120-G 323=080D8CE0-G 324=084E90B0-G 325=084EAF50-G 326=084EAE80-G 327=0988C920-G 328=084EACE0-G 329=084EAC10-G 330=084EAB40-G 331=084EAA70-G 332=09798770-G 333=086094D0-G 334=08609400-G 335=09798300-G 336=09798130-G 337=09798060-G 338=09799E50-G 339=09799D80-G 340=09E58F50-G 341=0860ADB0-G 342=0860ACE0-G 343=07FC54B0-G 344=07FC52E0-G 345=07FC5210-G 346=07FC5040-G 347=07FC4F50-G 348=084EBC10-G 349=084EBB40-G 350=084EBA70-G 351=084EB9A0-G 352=084EB8D0-G 353=084EB800-G 354=084EB730-G 355=084EB660-G 356=09EF4700-G 357=09EF4630-G 358=07F3C110-G 359=07F3DAD0-G 360=07F3DA00-G 361=07F3D930-G 362=084ECBA0-G 363=084EC7F0-G 364=084EC720-G 365=084EC650-G 366=080DA100-G 367=0AA4F200-G 368=080DBF50-G 369=080DBE80-G 370=08BBE7F0-G 371=08BBE720-G 372=080DBC10-G 373=080DBB40-G 374=08BBC6B0-G 375=08BBC5E0-G 376=080DB8D0-G 377=080DB800-G 378=0AA34AD0-G 379=0AA34A00-G 380=080DB590-G 381=080DB4C0-G 382=0AA34190-G 383=0AA340C0-G 384=0AA35B50-G 385=0AA35A80-G 386=0AA36F50-G 387=0AA36E80-G [0832C790-G] Name=H_GDI APC/engineer attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3E00-G TaskForce=0832C3F0-G [07EB8E90-G] Name=H_Nod APC/engineers attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3E00-G TaskForce=07ECC200-G [080646D0-G] Name=H_Nod APC/eng. steal money VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3980-G TaskForce=08063D20-G [080643E0-G] Name=H_GDI APC/eng. steal money VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3980-G TaskForce=08063DE0-G [084D4AE0-G] Name=H_GDI APC/commando attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075A3070-G TaskForce=0804C6C0-G [084D4730-G] Name=H_Nod APC/commando attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075A3070-G TaskForce=0804C530-G [0832D7F0-G] Name=H_Nod harvester attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=073A9510-G [0832D440-G] Name=H_Nod harvester attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=073A9CC0-G [07E7F400-G] Name=H_GDI harvester attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=07ECD1F0-G [0832D120-G] Name=H_GDI harvester attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=075BFDC0-G [07E7F0E0-G] Name=H_Nod base repair vehicle VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0859E2E0-G [07E89580-G] Name=H_Nod base defense attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=07EA4290-G [0832E770-G] Name=H_Nod base defense attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=073A8CF0-G [07ECE560-G] Name=H_Nod devil's tongue pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07ECE4A0-G [0832E3D0-G] Name=H_Nod tank pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=0786DA60-G TaskForce=073A8CF0-G [0832E300-G] Name=H_Nod ranged pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=0786DA60-G TaskForce=07EA4290-G [0832E230-G] Name=H_Nod recon pool 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=073A9510-G [0832E160-G] Name=H_Nod stealth pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=073A9CC0-G [090E0AC0-G] Name=H_GDI amphibious pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=084B2F60-G [090E0930-G] Name=H_GDI titan pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07ECFB60-G [090E07A0-G] Name=H_GDI wolverine pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07ECD1F0-G [090E0620-G] Name=H_GDI hover pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=075BFDC0-G [084B2AA0-G] Name=H_GDI base defense attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=075BFDC0-G [084B2910-G] Name=H_GDI base defense attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=096473E0-G [0B7D4E70-G] Name=H_GDI aerial base attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=08603140-G [0B7D4A90-G] Name=H_GDI aerial base attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=07EA1E90-G [0B7D48E0-G] Name=H_Nod aerial base attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=08602820-G [0B7D4730-G] Name=H_Nod aerial base attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=0860DE90-G [0B7D4580-G] Name=H_GDI ORCA fighter pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08603140-G [0B7D44A0-G] Name=H_GDI ORCA bomber pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07EA1E90-G [0B7D67E0-G] Name=H_Nod banshee pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0860DE90-G [0B7D6700-G] Name=H_Nod harpy pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08602820-G [07ECD3B0-G] Name=H_GDI vehicle attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=07ED0860-G [0753BE90-G] Name=H_GDI vehicle attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=07ECFB60-G [07EA0540-G] Name=H_GDI vehicle attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075BFDC0-G [073A8540-G] Name=H_GDI vehicle attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075BFDC0-G [084D7070-G] Name=H_GDI vehicle attack 4 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=07ED0460-G [07EA0150-G] Name=H_GDI vehicle attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=096473E0-G [090E2C20-G] Name=H_GDI disrupter pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=096473E0-G [07EA1E80-G] Name=H_GDI vehicle attack 6a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=096473E0-G [073A8070-G] Name=H_Nod vehicle attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073A8CF0-G [073A9E50-G] Name=H_Nod vehicle attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073A8CF0-G [073A9D80-G] Name=H_Nod vehicle attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073A9CC0-G [073A9BF0-G] Name=H_Nod vehicle attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073A9CC0-G [0B7F7500-G] Name=H_Nod aerial vehicle attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=0860DE90-G [0B7F7420-G] Name=H_GDI aerial vehicle attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08603140-G [084D8A40-G] Name=H_Nod APC/thief steal vehicles VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=0960AAA0-G TaskForce=08050A00-G [073A95D0-G] Name=H_Nod vehicle attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073A9510-G [073A97A0-G] Name=H_GDI infantry attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07ED1330-G [073A93F0-G] Name=H_GDI infantry attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07ECD1F0-G [073A9320-G] Name=H_GDI infantry attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=096473E0-G [073A83B0-G] Name=H_GDI infantry attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=096473E0-G [07EA2AC0-G] Name=H_Nod infantry attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=073AACA0-G [07EA2930-G] Name=H_Nod infantry attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=073AACA0-G [0965C310-G] Name=H_Nod infantry attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07ECE4A0-G [073AA970-G] Name=H_Nod infantry attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07ECE4A0-G [0753EC30-G] Name=H_GDI vehicle attack 6b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075E02F0-G [07EA22F0-G] Name=H_GDI vehicle attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=09F7B380-G [084D9710-G] Name=H_Nod vehicle attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073AA3F0-G [0753E780-G] Name=H_Nod vehicle attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075E2310-G [0753E5F0-G] Name=H_Nod vehicle attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075E2180-G [073AA2F0-G] Name=H_GDI infantry attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=09F7B380-G [073AA220-G] Name=H_GDI infantry attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=075E02F0-G [073AA150-G] Name=H_GDI infantry attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07E04DC0-G [0753E130-G] Name=H_Nod infantry attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07E04DC0-G [0753E060-G] Name=H_Nod infantry attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=075E2180-G [074A32F0-G] Name=H_Nod rocket infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=075E2310-G [073AB9B0-G] Name=H_Nod light infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07E04DC0-G [084DAA80-G] Name=H_Nod cyborg pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=075E2180-G [090E45F0-G] Name=H_GDI light infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07E04DC0-G [090E4520-G] Name=H_GDI jumpjet infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09F7B380-G [090E4F50-G] Name=H_GDI disc thrower pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=075E02F0-G [084DA6A0-G] Name=H_GDI con. yard attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07ED0860-G [0B7F17D0-G] Name=H_GDI con. yard attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07EA1E90-G [084DA2E0-G] Name=H_GDI con. yard attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=075BFDC0-G [084DA210-G] Name=H_GDI con. yard attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=09F7B380-G [084DA140-G] Name=H_GDI con. yard attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=096473E0-G [084DA070-G] Name=H_GDI con. yard attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=075E02F0-G [0B7F6CA0-G] Name=H_GDI con. yard attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08603140-G [0B7F6BC0-G] Name=H_GDI con. yard attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07EA1E90-G [084DBDB0-G] Name=H_Nod con. yard attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=073A8CF0-G [0B7F6A00-G] Name=H_Nod con. yard attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0860DE90-G [084DBB40-G] Name=H_Nod con. yard attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=073A9510-G [0B7F6840-G] Name=H_Nod con. yard attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08602820-G [084DB9A0-G] Name=H_Nod con. yard attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07ECE4A0-G [084DB8D0-G] Name=H_Nod con. yard attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07EA4290-G [084DB800-G] Name=H_Nod con. yard attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=073A9CC0-G [084DB730-G] Name=H_Nod con. yard attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=075E2310-G [073AC800-G] Name=H_GDI factories attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07ED0860-G [0B7F4B50-G] Name=H_GDI factories attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07EA1E90-G [073AC440-G] Name=H_GDI factories attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=075BFDC0-G [073ACE80-G] Name=H_GDI factories attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=09F7B380-G [073ACDB0-G] Name=H_GDI factories attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=096473E0-G [073AC1B0-G] Name=H_GDI factories attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=075E02F0-G [0B7F46F0-G] Name=H_GDI factories attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08603140-G [0B7F4610-G] Name=H_GDI factories attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07EA1E90-G [09649240-G] Name=H_Nod factories attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=073A8CF0-G [0B7F4450-G] Name=H_Nod factories attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0860DE90-G [0964EF50-G] Name=H_Nod factories attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=073A9510-G [0B7F8070-G] Name=H_Nod factories attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08602820-G [0964EBC0-G] Name=H_Nod factories attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07ECE4A0-G [0964EAF0-G] Name=H_Nod factories attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07EA4290-G [0964EA20-G] Name=H_Nod factories attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=073A9CC0-G [0964E950-G] Name=H_Nod factories attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=075E2310-G [07EF9730-G] Name=H_GDI tib. refinery attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07ED0860-G [04167990-G] Name=H_GDI tib. refinery attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07EA1E90-G [07EA6C60-G] Name=H_GDI tib. refinery attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=075BFDC0-G [07EA6B90-G] Name=H_GDI tib. refinery attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=09F7B380-G [07EF91D0-G] Name=H_GDI tib. refinery attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=096473E0-G [04174D70-G] Name=H_GDI tib. refinery attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08603140-G [075A55C0-G] Name=H_GDI tib. refinery attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=075E02F0-G [04174BB0-G] Name=H_GDI tib. refinery attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07EA1E90-G [07E89DD0-G] Name=H_Nod tib. refinery attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=073A8CF0-G [04176E60-G] Name=H_Nod tib. refinery attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0860DE90-G [073AEA00-G] Name=H_Nod tib. refinery attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=073A9510-G [04173E00-G] Name=H_Nod tib. refinery attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08602820-G [07E8F140-G] Name=H_Nod tib. refinery attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07ECE4A0-G [07E8CF50-G] Name=H_Nod tib. refinery attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07EA4290-G [073AE6C0-G] Name=H_Nod tib. refinery attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=073A9CC0-G [07E8CDB0-G] Name=H_Nod tib. refinery attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=075E2310-G [073AE790-G] Name=H_GDI power facility attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07ED0860-G [041742A0-G] Name=H_GDI power facility attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07EA1E90-G [073AEE80-G] Name=H_GDI power facility attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=075BFDC0-G [073AE260-G] Name=H_GDI power facility attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=09F7B380-G [073AE190-G] Name=H_GDI power facility attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=096473E0-G [073AE0C0-G] Name=H_GDI power facility attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=075E02F0-G [041768B0-G] Name=H_GDI power facility attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08603140-G [041767D0-G] Name=H_GDI power facility attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07EA1E90-G [0A9BBF50-G] Name=H_Nod power facility attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=073A8CF0-G [04181EE0-G] Name=H_Nod power facility attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0860DE90-G [0A9BBDB0-G] Name=H_Nod power facility attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=073A9510-G [04181D20-G] Name=H_Nod power facility attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08602820-G [0A9BBC10-G] Name=H_Nod power facility attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07ECE4A0-G [0A9BBB40-G] Name=H_Nod power facility attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07EA4290-G [0A9BBA70-G] Name=H_Nod power facility attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=073A9CC0-G [0A9BB9A0-G] Name=H_Nod power facility attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=075E2310-G [073AF400-G] Name=H_Nod subterranean APC pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=085EC2D0-G [073AF270-G] Name=H_Nod recon pool 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=073AACA0-G [084DE340-G] Name=H_GDI missile silo attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07ED0860-G [080F71A0-G] Name=H_GDI missile silo attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07EA1E90-G [084DFF50-G] Name=H_GDI missile silo attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=075BFDC0-G [084DEA70-G] Name=H_GDI missile silo attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=09F7B380-G [084DE9A0-G] Name=H_GDI missile silo attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=096473E0-G [084DFBF0-G] Name=H_GDI missile silo attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=075E02F0-G [080CCD80-G] Name=H_GDI missile silo attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08603140-G [080CCCA0-G] Name=H_GDI missile silo attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07EA1E90-G [084DF980-G] Name=H_Nod missile silo attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=073A8CF0-G [080CCAE0-G] Name=H_Nod missile silo attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0860DE90-G [084DF7E0-G] Name=H_Nod missile silo attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=073A9510-G [080CC920-G] Name=H_Nod missile silo attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08602820-G [084DF640-G] Name=H_Nod missile silo attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07ECE4A0-G [084DFE80-G] Name=H_Nod missile silo attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07EA4290-G [084DFDB0-G] Name=H_Nod missile silo attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=073A9CC0-G [084DFCE0-G] Name=H_Nod missile silo attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=075E2310-G [084DF2C0-G] Name=H_Nod engineer pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09EF0540-G [084DF1F0-G] Name=H_GDI engineer pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09EF0540-G [084DF120-G] Name=H_Nod mutant hijacker pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=073AA3F0-G [084DF050-G] Name=H_GDI ghoststalker pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09EF2BB0-G [084D7830-G] Name=H_Nod cyborg commando pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=yes Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09F11CE0-G [084E0F50-G] Name=H_GDI upgrade center attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07ED0860-G [080CEA80-G] Name=H_GDI upgrade center attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07EA1E90-G [084E08B0-G] Name=H_GDI upgrade center attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=075BFDC0-G [084E07E0-G] Name=H_GDI upgrade center attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=09F7B380-G [084E0710-G] Name=H_GDI upgrade center attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=096473E0-G [084E0640-G] Name=H_GDI upgrade center attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=075E02F0-G [080CE620-G] Name=H_GDI upgrade center attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08603140-G [080CE540-G] Name=H_GDI upgrade center attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07EA1E90-G [084E03D0-G] Name=H_Nod upgrade center attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=073A8CF0-G [080CE380-G] Name=H_Nod upgrade center attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0860DE90-G [084E0230-G] Name=H_Nod upgrade center attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=073A9510-G [080CE1C0-G] Name=H_Nod upgrade center attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08602820-G [084E0090-G] Name=H_Nod upgrade center attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07ECE4A0-G [084E0B90-G] Name=H_Nod upgrade center attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07EA4290-G [084E0AC0-G] Name=H_Nod upgrade center attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=073A9CC0-G [084E09F0-G] Name=H_Nod upgrade center attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=075E2310-G [080CF910-G] Name=E_GDI vehicle attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08600780-G [080CF780-G] Name=E_GDI vehicle attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08600780-G [080CF6B0-G] Name=E_GDI vehicle attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=0965B600-G [080CF520-G] Name=E_GDI vehicle attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=0965B600-G [084E1790-G] Name=E_GDI vehicle attack 4 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=07ED0460-G [080CF380-G] Name=E_GDI vehicle attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=086001F0-G [080CFC20-G] Name=E_GDI vehicle attack 6a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=0965D960-G [080C70C0-G] Name=E_GDI vehicle attack 6b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08600DC0-G [080D0D90-G] Name=E_GDI vehicle attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08601B60-G [08468050-G] Name=E_Nod vehicle attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=086019D0-G [07D73AA0-G] Name=E_Nod vehicle attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=086019D0-G [07D739D0-G] Name=E_Nod vehicle attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08601770-G [07D73840-G] Name=E_Nod vehicle attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=086015E0-G [07E4EA50-G] Name=E_Nod vehicle attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08601770-G [07E4CF50-G] Name=E_Nod vehicle attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08601380-G [086012B0-G] Name=E_Nod vehicle attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=0846A4D0-G [084E1530-G] Name=E_Nod vehicle attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=073AA3F0-G [084E1460-G] Name=E_Nod banshee pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0860DE90-G [097235F0-G] Name=E_Nod base repair vehicle pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0859E2E0-G [084E21C0-G] Name=E_Nod cyborg commando pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09F11CE0-G [090EDA60-G] Name=E_Nod cyborg pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08601380-G [090ED990-G] Name=E_Nod devil's tongue pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08602B40-G [084E3E80-G] Name=E_Nod engineer pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=086029B0-G [084E3CF0-G] Name=E_Nod harpy pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08602820-G [090ECA90-G] Name=E_Nod light infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08602640-G [084E3980-G] Name=E_Nod mutant hijacker pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=073AA3F0-G [090ED230-G] Name=E_Nod ranged pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=0786DA60-G TaskForce=097246D0-G [08602F50-G] Name=E_Nod recon pool 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0846A4D0-G [090EDE80-G] Name=E_Nod recon pool 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08602DC0-G [090EDCF0-G] Name=E_Nod rocket infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=086015E0-G [090EDC20-G] Name=E_Nod stealth pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08601770-G [084E32D0-G] Name=E_Nod subterranean APC pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=085EC2D0-G [090EEB00-G] Name=E_Nod tank pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=0786DA60-G TaskForce=086019D0-G [084E3130-G] Name=H_Nod base air defense 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=075E2310-G [084E3060-G] Name=H_Nod base air defense 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=073A9CC0-G [084E4F50-G] Name=H_GDI base air defense 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09F7B380-G [084E38B0-G] Name=H_GDI base air defense 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=075BFDC0-G [084E37E0-G] Name=E_GDI amphibious pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=084B2F60-G [090EEDB0-G] Name=E_GDI disc thrower pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08600DC0-G [090EECE0-G] Name=E_GDI disruptor pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=086001F0-G [084E4AE0-G] Name=E_GDI engineer pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=086029B0-G [084E4A10-G] Name=E_GDI ghostalker pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=09EF2BB0-G [090EE280-G] Name=E_GDI hover pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0965B600-G [090EE1B0-G] Name=E_GDI jumpjet infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08601B60-G [084E47A0-G] Name=E_GDI ORCA bomber pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=07EA1E90-G [084E4610-G] Name=E_GDI ORCA fighter pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08603140-G [090EE700-G] Name=E_GDI titan pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08600780-G [090EE630-G] Name=E_GDI wolverine pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=086039B0-G [07F2A9F0-G] Name=E_GDI aerial base attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=08603140-G [07F2A920-G] Name=E_GDI aerial base attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=07EA1E90-G [07F2A850-G] Name=E_GDI aerial vehicle attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08603140-G [084E5E80-G] Name=E_GDI APC/commando attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075A3070-G TaskForce=0804C6C0-G [07EF7DF0-G] Name=E_GDI APC/eng. steal money VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3980-G TaskForce=0805DAC0-G [07EF7D20-G] Name=E_GDI APC/engineer attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3E00-G TaskForce=0805DAC0-G [084E5B50-G] Name=E_GDI base air defense 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08601B60-G [084E5A80-G] Name=E_GDI base air defense 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0965B600-G [07EFA790-G] Name=E_GDI base defense attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=0965B600-G [07EF88B0-G] Name=E_GDI base defense attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=0965D960-G [084E43A0-G] Name=E_GDI con. yard attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08600780-G [084E42D0-G] Name=E_GDI con. yard attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07EA1E90-G [084E55C0-G] Name=E_GDI con. yard attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08601B60-G [084E54F0-G] Name=E_GDI con. yard attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0965D960-G [084E5420-G] Name=E_GDI con. yard attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0965D960-G [084E5350-G] Name=E_GDI con. yard attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08600DC0-G [084E5280-G] Name=E_GDI con. yard attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08603140-G [084E51B0-G] Name=E_GDI con. yard attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=07EA1E90-G [090F0340-G] Name=E_GDI light infantry pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=yes Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08602640-G [080D32A0-G] Name=E_GDI factories attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08600780-G [080D31D0-G] Name=E_GDI factories attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07EA1E90-G [080D3100-G] Name=E_GDI factories attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08601B60-G [080D3030-G] Name=E_GDI factories attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0965D960-G [080D4A70-G] Name=E_GDI factories attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0965D960-G [080D49A0-G] Name=E_GDI factories attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08600DC0-G [080D48D0-G] Name=E_GDI factories attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08603140-G [080D4800-G] Name=E_GDI factories attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07EA1E90-G [07E30830-G] Name=E_GDI harvester attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=086039B0-G [07E30760-G] Name=E_GDI harvester attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=0965B600-G [080D4590-G] Name=E_GDI infantry attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=086039B0-G [080D44C0-G] Name=E_GDI infantry attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=086039B0-G [080D43F0-G] Name=E_GDI infantry attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08601B60-G [080D4320-G] Name=E_GDI infantry attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=0965D960-G [080D4250-G] Name=E_GDI infantry attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=0965D960-G [080D4180-G] Name=E_GDI infantry attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08600DC0-G [080D40B0-G] Name=E_GDI infantry attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602640-G [084E64C0-G] Name=E_GDI missile silo attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08600780-G [084E63F0-G] Name=E_GDI missile silo attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07EA1E90-G [084E6320-G] Name=E_GDI missile silo attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08601B60-G [084E6250-G] Name=E_GDI missile silo attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0965D960-G [084E6180-G] Name=E_GDI missile silo attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0965D960-G [084E60B0-G] Name=E_GDI missile silo attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08600DC0-G [084E7F50-G] Name=E_GDI missile silo attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08603140-G [084E7E80-G] Name=E_GDI missile silo attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=07EA1E90-G [080D58D0-G] Name=E_GDI power facility attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08600780-G [080D5800-G] Name=E_GDI power facility attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07EA1E90-G [080D5730-G] Name=E_GDI power facility attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08601B60-G [080D5660-G] Name=E_GDI power facility attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0965D960-G [080D5590-G] Name=E_GDI power facility attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0965D960-G [080D54C0-G] Name=E_GDI power facility attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08600DC0-G [080D53F0-G] Name=E_GDI power facility attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08603140-G [080D5320-G] Name=E_GDI power facility attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=07EA1E90-G [07ECC9F0-G] Name=E_GDI tib. refinery attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08600780-G [07ECC920-G] Name=E_GDI tib. refinery attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07EA1E90-G [07ECC750-G] Name=E_GDI tib. refinery attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08601B60-G [07ECC680-G] Name=E_GDI tib. refinery attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0965D960-G [07ECC4B0-G] Name=E_GDI tib. refinery attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0965D960-G [07ECC3E0-G] Name=E_GDI tib. refinery attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08600DC0-G [07ECC210-G] Name=E_GDI tib. refinery attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08603140-G [07ECC140-G] Name=E_GDI tib.refinery attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=07EA1E90-G [084E70B0-G] Name=E_GDI upgrade center attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08600780-G [084D9250-G] Name=E_GDI upgrade center attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07EA1E90-G [084DF570-G] Name=E_GDI upgrade center attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08601B60-G [084DF4A0-G] Name=E_GDI upgrade center attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0965D960-G [084DF3D0-G] Name=E_GDI upgrade center attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0965D960-G [084E8F50-G] Name=E_GDI upgrade center attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08600DC0-G [084E8E80-G] Name=E_GDI upgrade center attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08603140-G [084E8DB0-G] Name=E_GDI upgrade center attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=07EA1E90-G [09D00530-G] Name=E_Nod aerial base attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=08602820-G [08607590-G] Name=E_Nod aerial base attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7D0D0-G TaskForce=0860DE90-G [086074C0-G] Name=E_Nod aerial vehicle attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=0860DE90-G [084E8A70-G] Name=E_Nod APC/commando attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075A3070-G TaskForce=0804C530-G [07E8D320-G] Name=E_Nod APC/eng. steal money VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3980-G TaskForce=08060740-G [07E8D250-G] Name=E_Nod APC/engineer attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=085F3E00-G TaskForce=08060740-G [084E8740-G] Name=E_Nod APC/thief steal vehicles VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=0960AAA0-G TaskForce=08050A00-G [084E8670-G] Name=E_Nod base air defense 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=086015E0-G [084E85A0-G] Name=E_Nod base air defense 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=3 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=08601770-G [084E84D0-G] Name=E_Nod con. yard attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=086019D0-G [084E8400-G] Name=E_Nod con. yard attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0860DE90-G [084E8330-G] Name=E_Nod con. yard attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0846A4D0-G [084E8260-G] Name=E_Nod con. yard attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08602820-G [084E8190-G] Name=E_Nod con. yard attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08602B40-G [084E80C0-G] Name=E_Nod con. yard attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=097246D0-G [084E9F50-G] Name=E_Nod con. yard attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08601770-G [084E9E80-G] Name=E_Nod con. yard attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=086015E0-G [080D7590-G] Name=E_Nod factories attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=086019D0-G [080D74C0-G] Name=E_Nod factories attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0860DE90-G [080D73F0-G] Name=E_Nod factories attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0846A4D0-G [080D7320-G] Name=E_Nod factories attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08602820-G [080D7250-G] Name=E_Nod factories attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08602B40-G [080D7180-G] Name=E_Nod factories attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=097246D0-G [080D70B0-G] Name=E_Nod factories attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08601770-G [080C8F50-G] Name=E_Nod factories attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=086015E0-G [086083F0-G] Name=E_Nod harvester attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=0846A4D0-G [09D6EF50-G] Name=E_Nod harvester attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7B2A0-G TaskForce=08601770-G [080CF110-G] Name=E_Nod infantry attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602DC0-G [07FCFC60-G] Name=E_Nod infantry attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602DC0-G [07FCFB90-G] Name=E_Nod infantry attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08601380-G [07FCFAC0-G] Name=E_Nod infantry attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602B40-G [08750120-G] Name=E_Nod infantry attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602B40-G [080D8CE0-G] Name=E_Nod infantry attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602640-G [084E90B0-G] Name=E_Nod missile silo attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=086019D0-G [084EAF50-G] Name=E_Nod missile silo attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0860DE90-G [084EAE80-G] Name=E_Nod missile silo attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0846A4D0-G [0988C920-G] Name=E_Nod missile silo attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08602640-G [084EACE0-G] Name=E_Nod missile silo attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08602B40-G [084EAC10-G] Name=E_Nod missile silo attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=097246D0-G [084EAB40-G] Name=E_Nod missile silo attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08601770-G [084EAA70-G] Name=E_Nod missile silo attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=086015E0-G [09798770-G] Name=E_Nod power facility attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=086019D0-G [086094D0-G] Name=E_Nod power facility attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0860DE90-G [08609400-G] Name=E_Nod power facility attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0846A4D0-G [09798300-G] Name=E_Nod power facility attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08602820-G [09798130-G] Name=E_Nod power facility attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08602B40-G [09798060-G] Name=E_Nod power facility attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=097246D0-G [09799E50-G] Name=E_Nod power facility attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08601770-G [09799D80-G] Name=E_Nod power facility attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=086015E0-G [09E58F50-G] Name=E_Nod tib. refinery attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=086019D0-G [0860ADB0-G] Name=E_Nod tib. refinery attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0860DE90-G [0860ACE0-G] Name=E_Nod tib. refinery attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0846A4D0-G [07FC54B0-G] Name=E_Nod tib. refinery attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08602820-G [07FC52E0-G] Name=E_Nod tib. refinery attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08602B40-G [07FC5210-G] Name=E_Nod tib. refinery attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=097246D0-G [07FC5040-G] Name=E_Nod tib. refinery attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08601770-G [07FC4F50-G] Name=E_Nod tib. refinery attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=086015E0-G [084EBC10-G] Name=E_Nod upgrade center attack 1a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=086019D0-G [084EBB40-G] Name=E_Nod upgrade center attack 1b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0860DE90-G [084EBA70-G] Name=E_Nod upgrade center attack 2a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0846A4D0-G [084EB9A0-G] Name=E_Nod upgrade center attack 2b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08602820-G [084EB8D0-G] Name=E_Nod upgrade center attack 3a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08602B40-G [084EB800-G] Name=E_Nod upgrade center attack 3b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=097246D0-G [084EB730-G] Name=E_Nod upgrade center attack 4a VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08601770-G [084EB660-G] Name=E_Nod upgrade center attack 4b VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=086015E0-G [09EF4700-G] Name=E_Nod base defense attack 1 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=086019D0-G [09EF4630-G] Name=E_Nod base defense attack 2 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=097246D0-G [07F3C110-G] Name=E_Nod APC/infantry attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F3DE00-G TaskForce=07F3CAE0-G [07F3DAD0-G] Name=E_GDI APC/infantry attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F3DE00-G TaskForce=07F3A490-G [07F3DA00-G] Name=H_Nod APC/infantry attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F3DE00-G TaskForce=07F3CAE0-G [07F3D930-G] Name=H_GDI APC/infantry attack VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=yes AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F3DE00-G TaskForce=07F3A490-G [084ECBA0-G] Name=E_GDI replace MCV VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-40094 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=08B50140-G TaskForce=0860B440-G [084EC7F0-G] Name=H_GDI replace MCV VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=no Priority=8 Max=1 TechLevel=0 Group=-40094 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=08B50140-G TaskForce=0860B440-G [084EC720-G] Name=E_Nod replace MCV VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=1 TechLevel=0 Group=-40094 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=08B50140-G TaskForce=0860B440-G [084EC650-G] Name=H_Nod replace MCV VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=no Priority=8 Max=1 TechLevel=0 Group=-40094 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=08B50140-G TaskForce=0860B440-G [080DA100-G] Name=E_GDI infantry attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08600DC0-G [0AA4F200-G] Name=H_GDI infantry attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=075E02F0-G [080DBF50-G] Name=E_GDI factories attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08602640-G [080DBE80-G] Name=E_GDI factories attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08600DC0-G [08BBE7F0-G] Name=H_GDI factories attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07E04DC0-G [08BBE720-G] Name=H_GDI factories attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=075E02F0-G [080DBC10-G] Name=E_GDI vehicle attack 8 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08602640-G [080DBB40-G] Name=E_GDI vehicle attack 9 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08600DC0-G [08BBC6B0-G] Name=H_GDI vehicle attack 8 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=07E04DC0-G [08BBC5E0-G] Name=H_GDI vehicle attack 9 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075E02F0-G [080DB8D0-G] Name=E_Nod factories attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08602640-G [080DB800-G] Name=E_Nod factories attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=086015E0-G [0AA34AD0-G] Name=H_Nod factories attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=07E04DC0-G [0AA34A00-G] Name=H_Nod factories attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=075E2310-G [080DB590-G] Name=E_Nod infantry attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=08602640-G [080DB4C0-G] Name=E_Nod infantry attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=086015E0-G [0AA34190-G] Name=H_Nod infantry attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=07E04DC0-G [0AA340C0-G] Name=H_Nod infantry attack 6 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F76BE0-G TaskForce=075E2310-G [0AA35B50-G] Name=E_Nod vehicle attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=08602640-G [0AA35A80-G] Name=E_Nod vehicle attack 8 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=086015E0-G [0AA36F50-G] Name=H_Nod vehicle attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=07E04DC0-G [0AA36E80-G] Name=H_Nod vehicle attack 8 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=yes IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7E3B0-G TaskForce=075E2310-G [AITriggerTypes] 08594AB0-G=H_Nod APC/engineer attack,07EB8E90-G,,6,-1,ENGINEER,0500000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,,0,0,1 085949B0-G=H_GDI APC/engineer attack,0832C790-G,,6,-1,ENGINEER,0500000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,,0,0,1 085948B0-G=H_Nod APC/eng. steal money,080646D0-G,,6,4,,b80b000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,,0,0,1 085947B0-G=H_GDI APC/eng. steal money,080643E0-G,,6,4,,b80b000004000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,,0,0,1 085946B0-G=H_Nod APC/commando attack,084D4730-G,,10,1,CYC2,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,,0,0,1 085945B0-G=H_GDI APC/commando attack,084D4AE0-G,,10,1,GHOST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,,0,0,1 074AC080-G=H_GDI harvester attack 1,07E7F400-G,,2,0,HORV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07E7F400-G,0,0,1 074ADF20-G=H_GDI harvester attack 2,0832D120-G,,7,0,HORV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,0832D120-G,0,0,1 085945D0-G=H_Nod harvester attack 1,0832D7F0-G,,5,0,HARV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0832D7F0-G,0,0,1 085944D0-G=H_Nod harvester attack 2,0832D440-G,,8,0,HORV,0200000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0832D440-G,0,0,1 085940B0-G=H_Nod base repair vehicle pool,07E7F0E0-G,,7,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,,0,0,1 08595F20-G=H_Nod base defense attack 1,07E89580-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,07E89580-G,0,0,1 08595E20-G=H_Nod base defense attack 2,0832E770-G,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0832E770-G,0,0,1 090EFC90-G=H_Nod ranged pool,0832E300-G,,6,1,NARADR,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E300-G,0,0,1 090EFB90-G=H_Nod devil's tongue pool,07ECE560-G,,7,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,07ECE560-G,0,0,1 090F8F20-G=H_Nod tank pool,0832E3D0-G,,3,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E3D0-G,0,0,1 090F8E20-G=H_Nod stealth pool,0832E160-G,,8,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,0832E160-G,0,0,1 090F8D20-G=H_GDI hover pool,090E0620-G,,7,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,090E0620-G,0,0,1 084EFF20-G=H_GDI aerial base attack 1,0B7D4E70-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,0,1 08595720-G=H_GDI base defense attack 1,084B2AA0-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084B2AA0-G,0,0,1 08595620-G=H_GDI base defense attack 2,084B2910-G,,9,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084B2910-G,0,0,1 084EFC20-G=H_GDI aerial base attack 2,0B7D4A90-G,,8,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,0,1 084EFB20-G=H_Nod aerial base attack 1,0B7D48E0-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,0,1 084EFA20-G=H_Nod aerial base attack 2,0B7D4730-G,,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,0,1 084EF920-G=H_GDI ORCA fighter pool,0B7D4580-G,,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,,0,0,1 084EF820-G=H_GDI ORCA bomber pool,0B7D44A0-G,,8,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,1,,0,0,1 084EF720-G=H_Nod banshee pool,0B7D67E0-G,,9,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,,0,0,1 084EF620-G=H_Nod harpy pool,0B7D6700-G,,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,1,,0,0,1 08596D20-G=H_GDI vehicle attack 1,07ECD3B0-G,,3,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,07ECD3B0-G,0,0,1 08596C20-G=H_GDI vehicle attack 2,0753BE90-G,,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,073A8540-G,0,0,1 08596B20-G=H_GDI vehicle attack 3,07EA0540-G,,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,,0,0,1 080DF140-G=H_GDI vehicle attack 4,084D7070-G,,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,,0,0,1 08596850-G=H_GDI vehicle attack 5,07EA0150-G,,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,07EA0150-G,0,0,1 08596750-G=H_GDI vehicle attack 6,07EA1E80-G,,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,0753EC30-G,0,0,1 08596650-G=H_Nod vehicle attack 1,073A8070-G,,3,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,073A8070-G,0,0,1 08596550-G=H_Nod vehicle attack 2,073A9E50-G,,3,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0753E780-G,0,0,1 08596450-G=H_Nod vehicle attack 3,073A9D80-G,,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,073A9D80-G,0,0,1 08596350-G=H_Nod vehicle attack 4,073A9BF0-G,,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0753E5F0-G,0,0,1 084F0B20-G=H_Nod aerial vehicle attack,0B7F7500-G,,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,0,1 084F0A20-G=H_GDI aerial vehicle attack,0B7F7420-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,0,1 08596050-G=H_Nod APC/thief steal vehicles,084D8A40-G,,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,,0,0,1 08597E20-G=H_Nod vehicle attack 5,073A95D0-G,,5,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,073A95D0-G,0,0,1 08597D20-G=H_GDI infantry attack 1,073A97A0-G,,2,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073A97A0-G,0,0,1 08597C20-G=H_GDI infantry attack 2,073A93F0-G,,6,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AA2F0-G,0,0,1 08597B20-G=H_GDI infantry attack 3,073A9320-G,,9,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073A9320-G,0,0,1 08597A20-G=H_GDI infantry attack 4,073A83B0-G,,9,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AA220-G,0,0,1 08597920-G=H_Nod infantry attack 1,07EA2AC0-G,,2,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07EA2AC0-G,0,0,1 08597820-G=H_Nod infantry attack 2,07EA2930-G,,4,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0753E060-G,0,0,1 08597720-G=H_GDI vehicle attack 7,07EA22F0-G,,6,-1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,07EA22F0-G,0,0,1 080DDBE0-G=H_Nod vehicle attack 6,084D9710-G,,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,,0,0,1 08597520-G=H_Nod infantry attack 3,0965C310-G,,7,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0965C310-G,0,0,1 090F9350-G=H_Nod rocket infantry pool,074A32F0-G,,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,1,074A32F0-G,0,0,1 090FA700-G=H_Nod light infantry pool,073AB9B0-G,,1,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,1,073AB9B0-G,0,0,1 090FA600-G=H_Nod cyborg pool,084DAA80-G,,4,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,1,084DAA80-G,0,0,1 090FA500-G=H_GDI light infantry pool,090E45F0-G,,1,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,1,090E45F0-G,0,0,1 080AB180-G=H_GDI con. yard attack 1,084DA6A0-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,0B7F17D0-G,0,0,1 08598F20-G=H_GDI con. yard attack 2,084DA2E0-G,,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084DA210-G,0,0,1 08598E20-G=H_GDI con. yard attack 3,084DA140-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084DA070-G,0,0,1 08598D20-G=H_GDI con. yard attack 4,0B7F6CA0-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,0B7F6BC0-G,0,0,1 08598C20-G=H_Nod con. yard attack 1,084DBDB0-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0B7F6A00-G,0,0,1 08598B20-G=H_Nod con. yard attack 2,084DBB40-G,,5,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0B7F6840-G,0,0,1 085988D0-G=H_Nod con. yard attack 3,084DB9A0-G,,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084DB8D0-G,0,0,1 085987D0-G=H_Nod con. yard attack 4,084DB800-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084DB730-G,0,0,1 085986D0-G=H_GDI factories attack 1,073AC800-G,,8,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,0B7F4B50-G,0,0,1 085985D0-G=H_GDI factories attack 2,073AC440-G,,7,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,073ACE80-G,0,0,1 085984D0-G=H_GDI factories attack 3,073ACDB0-G,,9,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,073AC1B0-G,0,0,1 085983D0-G=H_GDI factories attack 4,0B7F46F0-G,,8,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,0B7F4610-G,0,0,1 085982D0-G=H_Nod factories attack 1,09649240-G,,9,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0B7F4450-G,0,0,1 085981D0-G=H_Nod factories attack 2,0964EF50-G,,5,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0B7F8070-G,0,0,1 085980D0-G=H_Nod factories attack 3,0964EBC0-G,,7,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0964EAF0-G,0,0,1 08599F20-G=H_Nod factories attack 4,0964EA20-G,,8,-1,PROC,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0964E950-G,0,0,1 08598A20-G=H_GDI infantry attack 5,073AA150-G,,1,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AA150-G,0,0,1 0AAC5B20-G=H_GDI tib. refinery attack 1,07EF9730-G,,8,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,04167990-G,0,0,1 075A5EC0-G=H_GDI tib. refinery attack 2,07EA6C60-G,,7,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,07EA6B90-G,0,0,1 075A53F0-G=H_GDI tib. refinery attack 3,07EF91D0-G,,9,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,075A55C0-G,0,0,1 075A52F0-G=H_GDI tib. refinery attack 4,04174D70-G,,8,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,0,1 07E8CCB0-G=H_Nod tib. refinery attack 1,07E89DD0-G,,9,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,04176E60-G,0,0,1 07E8CBB0-G=H_Nod tib. refinery attack 2,073AEA00-G,,5,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,04173E00-G,0,0,1 07E8CAB0-G=H_Nod tib. refinery attack 3,07E8F140-G,,7,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,07E8CF50-G,0,0,1 07E8C9B0-G=H_Nod tib. refinery attack 4,073AE6C0-G,,8,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,07E8CDB0-G,0,0,1 084712B0-G=H_GDI power facility attack 1,073AE790-G,,8,2,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,041742A0-G,0,0,1 08471C20-G=H_GDI power facility attack 2,073AEE80-G,,7,3,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AE260-G,0,0,1 08472F20-G=H_GDI power facility attack 3,073AE190-G,,9,2,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,073AE0C0-G,0,0,1 08472E20-G=H_GDI power facility attack 4,041768B0-G,,8,3,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,041767D0-G,0,0,1 08472D20-G=H_Nod power facility attack 1,0A9BBF50-G,,9,3,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,04181EE0-G,0,0,1 08472C20-G=H_Nod power facility attack 2,0A9BBDB0-G,,5,3,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,04181D20-G,0,0,1 08472B20-G=H_Nod power facility attack 3,0A9BBC10-G,,7,2,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A9BBB40-G,0,0,1 08472A20-G=H_Nod power facility attack 4,0A9BBA70-G,,8,2,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A9BB9A0-G,0,0,1 0859AB20-G=H_Nod infantry attack 4,073AA970-G,,7,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,073AA220-G,0,0,1 090FDF20-G=H_GDI amphibious pool,090E0AC0-G,,6,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E0AC0-G,0,0,1 090FDE20-G=H_GDI disc thrower pool,090E4F50-G,,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E4F50-G,0,0,1 090FC820-G=H_GDI disruptor pool,090E2C20-G,,9,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,090E2C20-G,0,0,1 090FDB60-G=H_GDI jumpjet infantry pool,090E4520-G,,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E4520-G,0,0,1 090FDA60-G=H_GDI titan pool,090E0930-G,,3,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,090E0930-G,0,0,1 090FD960-G=H_GDI wolverine pool,090E07A0-G,,2,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,090E07A0-G,0,0,1 090FD860-G=H_Nod recon pool 2,073AF270-G,,2,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,073AF270-G,0,0,1 090FD760-G=H_Nod subterranean APC pool,073AF400-G,,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,073AF400-G,0,0,1 0859A160-G=H_GDI missile silo attack 1,084DE340-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080F71A0-G,0,0,1 0859A060-G=H_GDI missile silo attack 2,084DFF50-G,,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084DEA70-G,0,0,1 0859BF20-G=H_GDI missile silo attack 3,084DE9A0-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084DFBF0-G,0,0,1 0859BE20-G=H_GDI missile silo attack 4,080CCD80-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080CCCA0-G,0,0,1 0859A820-G=H_Nod missile silo attack 1,084DF980-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CCAE0-G,0,0,1 0859BB30-G=H_Nod missile silo attack 2,084DF7E0-G,,5,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CC920-G,0,0,1 0859BA30-G=H_Nod missile silo attack 3,084DF640-G,,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084DFE80-G,0,0,1 0859B930-G=H_Nod missile silo attack 4,084DFDB0-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084DFCE0-G,0,0,1 084F4230-G=H_Nod engineer pool ,084DF2C0-G,,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,0,1 084F4130-G=H_GDI engineer pool,084DF1F0-G,,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,0,1 0859B630-G=H_Nod mutant hijacker pool,084DF120-G,,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,0,1 074B3330-G=H_GDI ghostalker pool,084DF050-G,,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,0,1 0859B430-G=H_Nod cyborg commando pool,084D7830-G,,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,0,1 0859B330-G=H_GDI upgrade center attack 1,084E0F50-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080CEA80-G,0,0,1 0859BD20-G=H_GDI upgrade center attack 2,084E08B0-G,,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084E07E0-G,0,0,1 0859CF20-G=H_GDI upgrade center attack 3,084E0710-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,084E0640-G,0,0,1 0859CE20-G=H_GDI upgrade center attack 4,080CE620-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,080CE540-G,0,0,1 0859CD20-G=H_Nod upgrade center attack 1,084E03D0-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CE380-G,0,0,1 0859CC20-G=H_Nod upgrade center attack 2,084E0230-G,,5,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,080CE1C0-G,0,0,1 0859CB20-G=H_Nod upgrade center attack 3,084E0090-G,,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084E0B90-G,0,0,1 0859CA20-G=H_Nod upgrade center attack 4,084E0AC0-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,084E09F0-G,0,0,1 0859C920-G=E_GDI vehicle attack 1,080CF910-G,,3,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF910-G,1,0,0 0859C820-G=E_GDI vehicle attack 2,080CF780-G,,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF6B0-G,1,0,0 0859C720-G=E_GDI vehicle attack 3,080CF520-G,,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF520-G,1,0,0 0AF3EF10-G=E_GDI vehicle attack 4,084E1790-G,,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,,1,0,0 0859B130-G=E_GDI vehicle attack 5,080CF380-G,,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080CF380-G,1,0,0 0859B030-G=E_GDI vehicle attack 6,080CFC20-G,,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080C70C0-G,1,0,0 0859C3E0-G=E_GDI vehicle attack 7,080D0D90-G,,6,-1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D0D90-G,1,0,0 0859C2E0-G=E_Nod vehicle attack 1,08468050-G,,3,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,08468050-G,1,0,0 0859C1E0-G=E_Nod vehicle attack 2,07D73AA0-G,,3,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07D73840-G,1,0,0 0859C0E0-G=E_Nod vehicle attack 3,07D739D0-G,,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07D739D0-G,1,0,0 0859DF20-G=E_Nod vehicle attack 4,07E4EA50-G,,8,-1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,07E4CF50-G,1,0,0 0859DE20-G=E_Nod vehicle attack 5,086012B0-G,,5,-1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,086012B0-G,1,0,0 080E2360-G=E_Nod vehicle attack 6,084E1530-G,,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,1,0,0 07E8BE50-G=H_Nod base air defense 1,084E3130-G,,8,0,SCRIN,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1 07E8BD50-G=H_Nod base air defense 2,084E3130-G,,8,0,ORCAB,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1 07E8BC50-G=H_Nod base air defense 3,084E3130-G,,8,0,APACHE,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1 07E8BB50-G=H_Nod base air defense 4,084E3130-G,,8,0,ORCA,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1 084756C0-G=H_GDI base air defense 1,084E4F50-G,,7,0,SCRIN,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1 07E8B410-G=H_GDI base air defense 2,084E4F50-G,,7,0,ORCAB,0200000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1 07E8B310-G=H_GDI base air defense 3,084E4F50-G,,7,0,APACHE,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1 07E8B210-G=H_GDI base air defense 4,084E4F50-G,,7,0,ORCA,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1 084F7D20-G=E_Nod banshee pool,084E1460-G,,9,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,1,0,0 0859D2C0-G=E_Nod base repair vehicle pool,097235F0-G,,7,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,1,0,0 07D5E8A0-G=E_Nod cyborg commando pool,084E21C0-G,,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,,1,0,0 09100F20-G=E_Nod cyborg pool,090EDA60-G,,4,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090EDA60-G,1,0,0 07F232E0-G=E_Nod engineer pool,084E3E80-G,,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,,1,0,0 09101F20-G=E_Nod devil's tongue pool,090ED990-G,,7,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090ED990-G,1,0,0 0859ED20-G=E_Nod harpy pool,084E3CF0-G,,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,1,0,0 09101D20-G=E_Nod light infantry pool,090ECA90-G,,1,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090ECA90-G,1,0,0 07D70D20-G=E_Nod mutant hijacker pool,084E3980-G,,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,,1,0,0 09101B20-G=E_Nod ranged pool,090ED230-G,,6,1,NARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090ED230-G,1,0,0 09101A20-G=E_Nod recon pool 1,08602F50-G,,5,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,08602F50-G,1,0,0 09101920-G=E_Nod recon pool 2,090EDE80-G,,2,1,NAWEAP,0000000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090EDE80-G,1,0,0 091002C0-G=E_Nod rocket infantry pool,090EDCF0-G,,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,090EDCF0-G,1,0,0 091001C0-G=E_Nod stealth pool,090EDC20-G,,8,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,090EDC20-G,1,0,0 07D70470-G=E_Nod subterranean APC pool,084E32D0-G,,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,1,0,0 09101470-G=E_Nod tank pool,090EEB00-G,,3,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,090EEB00-G,1,0,0 0859E270-G=E_GDI aerial base attack 1,07F2A9F0-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07F2A9F0-G,1,0,0 0859E170-G=E_GDI aerial base attack 2,07F2A920-G,,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,07EF88B0-G,1,0,0 0859E070-G=E_GDI aerial vehicle attack,07F2A850-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07F2A850-G,1,0,0 0859FF20-G=E_GDI APC/commando attack,084E5E80-G,,10,1,GHOST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,,1,0,0 0859FE20-G=E_GDI APC/eng. steal money,07EF7DF0-G,,6,4,,8813000004000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,,1,0,0 0859FD20-G=E_GDI APC/engineer attack,07EF7D20-G,,6,-1,ENGINEER,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,,1,0,0 07EF8B30-G=E_GDI base air defense 1,084E5B50-G,,7,0,SCRIN,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0 07EF8A30-G=E_GDI base air defense 2,084E5B50-G,,7,0,ORCAB,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0 0859F940-G=E_GDI base air defense 3,084E5B50-G,,7,0,APACHE,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0 0859F840-G=E_GDI base air defense 4,084E5B50-G,,7,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0 0859F740-G=E_GDI base defense attack 1,07EFA790-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07EFA790-G,1,0,0 0859F640-G=E_GDI base defense attack 2,07EF88B0-G,,9,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07EF88B0-G,1,0,0 0859F540-G=E_GDI con. yard attack 1,084E43A0-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E42D0-G,1,0,0 0859F440-G=E_GDI con. yard attack 2,084E55C0-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E54F0-G,1,0,0 0859F340-G=E_GDI con. yard attack 3,084E5420-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E5350-G,1,0,0 0859F240-G=E_GDI con. yard attack 4,084E5280-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,084E51B0-G,1,0,0 096E8880-G=E_GDI amphibious pool,084E37E0-G,,6,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,1,0,0 09102C20-G=E_GDI disc thrower pool,090EEDB0-G,,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090EEDB0-G,1,0,0 09103F20-G=E_GDI disruptor pool,090EECE0-G,,9,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EECE0-G,1,0,0 096E8580-G=E_GDI engineer pool,084E4AE0-G,,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,,1,0,0 096E8480-G=E_GDI ghostalker pool,084E4A10-G,,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,,1,0,0 09103C20-G=E_GDI hover pool,090EE280-G,,7,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE280-G,1,0,0 09103B20-G=E_GDI jumpjet infantry pool,090EE1B0-G,,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090EE1B0-G,1,0,0 09103A20-G=E_GDI light infantry pool,090F0340-G,,1,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090F0340-G,1,0,0 085A0420-G=E_GDI ORCA bomber pool,084E47A0-G,,8,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,1,0,0 085A0320-G=E_GDI ORCA fighter pool,084E4610-G,,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,1,0,0 09102240-G=E_GDI titan pool,090EE700-G,,3,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,090EE700-G,1,0,0 09102140-G=E_GDI wolverine pool ,090EE630-G,,2,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,090EE630-G,1,0,0 085A0F20-G=E_GDI factories attack 1,080D32A0-G,,8,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D31D0-G,1,0,0 085A0E20-G=E_GDI factories attack 2,080D3100-G,,9,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D3030-G,1,0,0 085A0D20-G=E_GDI factories attack 3,080D4A70-G,,9,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D49A0-G,1,0,0 085A1BF0-G=E_GDI factories attack 4,080D48D0-G,,8,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080D4800-G,1,0,0 07E30660-G=E_GDI harvester attack 1,07E30830-G,,2,0,HARV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07E30830-G,1,0,0 07E30560-G=E_GDI harvester attack 2,07E30760-G,,7,0,HORV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,07E30760-G,1,0,0 085A18F0-G=E_GDI infantry attack 1,080D4590-G,,2,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D4590-G,1,0,0 085A17F0-G=E_GDI infantry attack 2,080D44C0-G,,6,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D43F0-G,1,0,0 085A1F20-G=E_GDI infantry attack 3,080D4320-G,,9,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D4320-G,1,0,0 085A1E20-G=E_GDI infantry attack 4,080D4250-G,,9,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D4180-G,1,0,0 085A1D20-G=E_GDI infantry attack 5,080D40B0-G,,1,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D40B0-G,1,0,0 085A13A0-G=E_GDI missile silo attack 1,084E64C0-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E63F0-G,1,0,0 085A12A0-G=E_GDI missile silo attack 2,084E6320-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E6250-G,1,0,0 085A11A0-G=E_GDI missile silo attack 3,084E6180-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E60B0-G,1,0,0 085A10A0-G=E_GDI missile silo attack 4,084E7F50-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E7E80-G,1,0,0 0856AF20-G=E_GDI power facility attack 1,080D58D0-G,,8,2,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D5800-G,1,0,0 07E57AE0-G=E_GDI power facility attack 2,080D5730-G,,9,3,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080D5660-G,1,0,0 0856AD20-G=E_GDI power facility attack 3,080D5590-G,,9,2,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,080D54C0-G,1,0,0 085696F0-G=E_GDI power facility attack 4,080D53F0-G,,8,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,080D5320-G,1,0,0 07ECC820-G=E_GDI tib. refinery attack 1,07ECC9F0-G,,8,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC920-G,1,0,0 07ECC580-G=E_GDI tib. refinery attack 2,07ECC750-G,,9,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC680-G,1,0,0 07ECC2E0-G=E_GDI tib. refinery attack 3,07ECC4B0-G,,9,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC3E0-G,1,0,0 07ECC040-G=E_GDI tib. refinery attack 4,07ECC210-G,,8,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07ECC140-G,1,0,0 085A26A0-G=E_GDI upgrade center attack 1,084E70B0-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084D9250-G,1,0,0 085A25A0-G=E_GDI upgrade center attack 2,084DF570-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084DF4A0-G,1,0,0 085A24A0-G=E_GDI upgrade center attack 3,084DF3D0-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E8F50-G,1,0,0 085A23A0-G=E_GDI upgrade center attack 4,084E8E80-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,084E8DB0-G,1,0,0 085A22A0-G=E_Nod aerial base attack 1,09D00530-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,09D00530-G,1,0,0 085A21A0-G=E_Nod aerial base attack 2,08607590-G,,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,08607590-G,1,0,0 085A20A0-G=E_Nod aerial vehicle attack ,086074C0-G,,9,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086074C0-G,1,0,0 085A2C20-G=E_Nod APC/commando attack,084E8A70-G,,10,1,CYC2,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,,1,0,0 085A2B20-G=E_Nod APC/eng. steal money ,07E8D320-G,,6,4,,8813000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,,1,0,0 085A2A20-G=E_Nod APC/engineer attack,07E8D250-G,,6,-1,ENGINEER,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,,1,0,0 085A3B80-G=E_Nod APC/thief steal vehicles,084E8740-G,,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,,1,0,0 07EA54C0-G=E_Nod base air defense 1,084E8670-G,,8,0,SCRIN,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0 07EA53C0-G=E_Nod base air defense 2,084E8670-G,,8,0,ORCAB,0400000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0 085A3880-G=E_Nod base air defense 3,084E8670-G,,8,0,APACHE,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0 085A3780-G=E_Nod base air defense 4,084E8670-G,,8,0,ORCA,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0 085A32B0-G=E_Nod con. yard attack 1,084E84D0-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E8400-G,1,0,0 085A31B0-G=E_Nod con. yard attack 2,084E8330-G,,5,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E8260-G,1,0,0 085A30B0-G=E_Nod con. yard attack 3,084E8190-G,,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E80C0-G,1,0,0 085A3F20-G=E_Nod con. yard attack 4,084E9F50-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,084E9E80-G,1,0,0 085A3E20-G=E_Nod factories attack 1,080D7590-G,,9,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080D74C0-G,1,0,0 085A3D20-G=E_Nod factories attack 2,080D73F0-G,,5,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080D7320-G,1,0,0 085A4F20-G=E_Nod factories attack 3,080D7250-G,,7,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080D7180-G,1,0,0 085A4E20-G=E_Nod factories attack 4,080D70B0-G,,8,-1,PROC,0300000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080C8F50-G,1,0,0 085A4D20-G=E_Nod harvester attack 1,086083F0-G,,5,0,HARV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086083F0-G,1,0,0 085A4C20-G=E_Nod harvester attack 2,09D6EF50-G,,8,0,HARV,0400000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09D6EF50-G,1,0,0 085A3680-G=E_Nod infantry attack 1,080CF110-G,,2,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080CF110-G,1,0,0 085A3580-G=E_Nod infantry attack 2,07FCFC60-G,,4,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,07FCFB90-G,1,0,0 085A3480-G=E_Nod infantry attack 3,07FCFAC0-G,,7,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,07FCFAC0-G,1,0,0 085A4730-G=E_Nod infantry attack 4,08750120-G,,7,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080D8CE0-G,1,0,0 085A4630-G=E_Nod missile silo attack 1,084E90B0-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EAF50-G,1,0,0 085A4530-G=E_Nod missile silo attack 2,084EAE80-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0988C920-G,1,0,0 085A4430-G=E_Nod missile silo attack 3,084EACE0-G,,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EAC10-G,1,0,0 097985A0-G=E_Nod power facility attack 1,09798770-G,,9,3,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,086094D0-G,1,0,0 085A4230-G=E_Nod missile silo attack 4,084EAB40-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EAA70-G,1,0,0 09798200-G=E_Nod power facility attack 2,08609400-G,,5,3,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09798300-G,1,0,0 09799F20-G=E_Nod power facility attack 3,09798130-G,,7,2,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09798060-G,1,0,0 09799C80-G=E_Nod power facility attack 4,09799E50-G,,8,2,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,09799D80-G,1,0,0 07FC5650-G=E_Nod tib. refinery attack 1,09E58F50-G,,9,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,0860ADB0-G,1,0,0 07FC53B0-G=E_Nod tib. refinery attack 2,0860ACE0-G,,5,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,07FC54B0-G,1,0,0 07FC5110-G=E_Nod tib. refinery attack 3,07FC52E0-G,,7,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,07FC5210-G,1,0,0 07FC4E50-G=E_Nod tib. refinery attack 4,07FC5040-G,,8,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,07FC4F50-G,1,0,0 085A5D20-G=E_Nod upgrade center attack 1,084EBC10-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EBB40-G,1,0,0 085A5C20-G=E_Nod upgrade center attack 2,084EBA70-G,,5,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EB9A0-G,1,0,0 085A5B20-G=E_Nod upgrade center attack 3,084EB8D0-G,,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EB800-G,1,0,0 085A5A20-G=E_Nod upgrade center attack 4,084EB730-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,084EB660-G,1,0,0 080CF220-G=M_GDI aerial base attack 1,0B7D4E70-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 080CF120-G=M_GDI aerial base attack 2,0B7D4A90-G,,8,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 080C71E0-G=M_GDI aerial vehicle attack,0B7F7420-G,,5,-1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080C70E0-G=M_GDI amphibious pool,090E0AC0-G,,6,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 085A5520-G=M_GDI APC/commando attack,084D4AE0-G,,10,1,GHOST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 085A5420-G=M_GDI APC/eng. steal money,080643E0-G,,6,4,ENGINEER,a00f000004000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,1,,0,1,0 085A5320-G=M_GDI APC/engineer attack,0832C790-G,,6,-1,ENGINEER,0500000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 080D0C20-G=M_GDI base air defense 1,084E4F50-G,,7,0,SCRIN,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E38B0-G,0,1,0 080D0B20-G=M_GDI base air defense 2,084E38B0-G,,7,0,ORCAB,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E4F50-G,0,1,0 080D0A20-G=M_GDI base air defense 4,084E38B0-G,,7,0,ORCA,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E4F50-G,0,1,0 080D0920-G=M_GDI base defense attack 1,084B2AA0-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D0820-G=M_GDI base defense attack 2,084B2910-G,,9,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D0720-G=M_GDI con. yard attack 1,084DA6A0-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,0B7F17D0-G,0,1,0 080D0620-G=M_GDI con. yard attack 2,084DA2E0-G,,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 080D0520-G=M_GDI con. yard attack 3,084DA140-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,084DA070-G,0,1,0 080D0420-G=M_GDI con. yard attack 4,0B7F6CA0-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,0B7F6BC0-G,0,1,0 080D0320-G=M_GDI disc thrower pool,090E4F50-G,,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 080D0220-G=M_GDI disruptor pool,090E2C20-G,,9,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D0120-G=M_GDI engineer pool,084DF1F0-G,,2,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1F20-G=M_GDI factories attack 1,073AC800-G,,8,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,0B7F4B50-G,0,1,0 080D1E20-G=M_GDI factories attack 2,073AC440-G,,7,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073ACE80-G,0,1,0 080D1D20-G=M_GDI factories attack 3,073ACDB0-G,,9,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073AC1B0-G,0,1,0 080D1C20-G=M_GDI factories attack 4,0B7F46F0-G,,8,-1,PROC,0200000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,0B7F4610-G,0,1,0 07F26720-G=M_GDI ghoststalker pool,084DF050-G,,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1A20-G=M_GDI harvester attack 1,07E7F400-G,,2,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1920-G=M_GDI harvester attack 2,0832D120-G,,7,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1820-G=M_GDI hover pool,090E0620-G,,7,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D1720-G=M_GDI infantry attack 1,073A97A0-G,,2,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1620-G=M_GDI infantry attack 2,073A93F0-G,,6,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AA2F0-G,0,1,0 080D1520-G=M_GDI infantry attack 3,073A9320-G,,7,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1420-G=M_GDI infantry attack 4,073A83B0-G,,9,-1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AA220-G,0,1,0 080D1320-G=M_GDI infantry attack 5,073AA150-G,,1,-1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D1220-G=M_GDI jumpjet infantry pool,090E4520-G,,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 080D1120-G=M_GDI light infantry pool,090E45F0-G,,1,1,GAPILE,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 080D2F20-G=M_GDI missile silo attack 1,084DE340-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080F71A0-G,0,1,0 080D2E20-G=M_GDI missile silo attack 2,084DFF50-G,,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084DEA70-G,0,1,0 080D2D20-G=M_GDI missile silo attack 3,084DE9A0-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084DFBF0-G,0,1,0 080D2C20-G=M_GDI missile silo attack 4,080CCD80-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080CCCA0-G,0,1,0 08597420-G=M_GDI ORCA bomber pool,0B7D44A0-G,,8,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 0859F140-G=M_GDI ORCA fighter pool,0B7D4580-G,,5,1,GAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D2920-G=M_GDI power facility attack 1,073AE790-G,,8,2,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,041742A0-G,0,1,0 080D2820-G=M_GDI power facility attack 2,073AEE80-G,,7,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AE260-G,0,1,0 080D2720-G=M_GDI power facility attack 3,073AE190-G,,9,2,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,073AE0C0-G,0,1,0 080D2620-G=M_GDI power facility attack 4,041768B0-G,,8,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,041767D0-G,0,1,0 080D2520-G=M_GDI tib. refinery attack 1,07EF9730-G,,8,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,04167990-G,0,1,0 080D2420-G=M_GDI tib. refinery attack 2,07EA6C60-G,,7,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,07EA6B90-G,0,1,0 080D2320-G=M_GDI tib. refinery attack 3,07EF91D0-G,,9,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,04174D70-G,0,1,0 080D2220-G=M_GDI tib. refinery attack 4,04174D70-G,,8,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,04174BB0-G,0,1,0 080D2120-G=M_GDI titan pool,090E0930-G,,3,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D3F20-G=M_GDI upgrade center attack 1,084E0F50-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080CEA80-G,0,1,0 080D3E20-G=M_GDI upgrade center attack 2,084E08B0-G,,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084E07E0-G,0,1,0 080D3D20-G=M_GDI upgrade center attack 3,084E0710-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,084E0640-G,0,1,0 080D3C20-G=M_GDI upgrade center attack 4,080CE620-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,080CE540-G,0,1,0 080D3B20-G=M_GDI vehicle attack 1,07ECD3B0-G,,3,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D3A20-G=M_GDI vehicle attack 2,0753BE90-G,,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,073A8540-G,0,1,0 080D3920-G=M_GDI vehicle attack 3,07EA0540-G,,7,-1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080F9470-G=M_GDI vehicle attack 4,084D7070-G,,10,1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D3720-G=M_GDI vehicle attack 5,07EA0150-G,,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080D3620-G=M_GDI vehicle attack 6,07EA1E80-G,,9,-1,GATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,0753EC30-G,0,1,0 080D3520-G=M_GDI vehicle attack 7,07EA22F0-G,,6,-1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 08500120-G=M_GDI wolverine pool ,090E07A0-G,,2,1,GAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 0914DE20-G=H_Nod recon pool 1,0832E230-G,,5,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0832E230-G,0,0,1 09EF4530-G=E_Nod base defense attack 1,09EF4700-G,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,09EF4700-G,1,0,0 09EF4430-G=E_Nod base defense attack 2,09EF4630-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,09EF4630-G,1,0,0 080D4F20-G=M_Nod aerial base attack 1,0B7D48E0-G,,5,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 080D4E20-G=M_Nod aerial base attack 2,0B7D4730-G,,9,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 080D4D20-G=M_Nod aerial vehicle attack,0B7F7500-G,,9,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 09F30510-G=M_Nod APC/commando attack,084D4730-G,,10,1,CYC2,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,,0,1,0 09F30410-G=M_Nod APC/eng. steal money,080646D0-G,,6,4,,a00f000004000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 09F30310-G=M_Nod APC/engineer attack,07EB8E90-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,,0,1,0 09F30210-G=M_Nod APC/thief steal vehicles,084D8A40-G,,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,,0,1,0 080D4820-G=M_Nod base air defense 1,084E3130-G,,8,0,SCRIN,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3060-G,0,1,0 080D4720-G=M_Nod base air defense 2,084E3060-G,,8,0,ORCAB,0300000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3130-G,0,1,0 080D4620-G=M_Nod base air defense 3,084E3130-G,,8,0,APACHE,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3060-G,0,1,0 080D4520-G=M_Nod base air defense 4,084E3060-G,,8,0,ORCA,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3130-G,0,1,0 080D4420-G=M_Nod base defense attack 1,07E89580-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080D4320-G=M_Nod base defense attack 2,0832E770-G,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080D4220-G=M_Nod con. yard attack 1,084DBDB0-G,,9,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,0B7F6A00-G,0,1,0 080D4120-G=M_Nod con. yard attack 2,084DBB40-G,,5,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,0B7F6840-G,0,1,0 080D5F20-G=M_Nod con. yard attack 3,084DB9A0-G,,7,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,084DB8D0-G,0,1,0 080D5E20-G=M_Nod con. yard attack 4,084DB800-G,,8,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,084DB730-G,0,1,0 080D5D20-G=M_Nod factories attack 1,09649240-G,,8,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0B7F4450-G,0,1,0 080D5C20-G=M_Nod factories attack 2,0964EF50-G,,5,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0B7F8070-G,0,1,0 080D5B20-G=M_Nod factories attack 3,0964EBC0-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0964EAF0-G,0,1,0 080D5A20-G=M_Nod factories attack 4,0964EA20-G,,8,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0964E950-G,0,1,0 080D5920-G=M_Nod harvester attack 1,0832D7F0-G,,5,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080D5820-G=M_Nod harvester attack 2,0832D440-G,,8,0,HARV,0300000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080D5720-G=M_Nod infantry attack 1,07EA2AC0-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080D5620-G=M_Nod infantry attack 2,07EA2930-G,,4,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0753E060-G,0,1,0 080D5520-G=M_Nod infantry attack 3,0965C310-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080D5420-G=M_Nod infantry attack 4,073AA970-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0753E130-G,0,1,0 080D5320-G=M_Nod missile silo attack 1,084DF980-G,,9,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CCAE0-G,0,1,0 080D5220-G=M_Nod missile silo attack 2,084DF7E0-G,,5,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CC920-G,0,1,0 080D5120-G=M_Nod missile silo attack 3,084DF640-G,,7,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084DFE80-G,0,1,0 080D6F20-G=M_Nod missile silo attack 4,084DFDB0-G,,8,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084DFCE0-G,0,1,0 080D6E20-G=M_Nod power facility attack 1,0A9BBF50-G,,9,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,04181EE0-G,0,1,0 080D6D20-G=M_Nod power facility attack 2,0A9BBDB0-G,,5,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,04181D20-G,0,1,0 080D6C20-G=M_Nod power facility attack 3,0A9BBC10-G,,7,2,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0A9BBB40-G,0,1,0 080D6B20-G=M_Nod power facility attack 4,0A9BBA70-G,,8,2,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,0A9BB9A0-G,0,1,0 080D6A20-G=M_Nod tib. refinery attack 1,07E89DD0-G,,9,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,04176E60-G,0,1,0 080D6920-G=M_Nod tib. refinery attack 2,073AEA00-G,,5,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,04173E00-G,0,1,0 080D6820-G=M_Nod tib. refinery attack 3,07E8F140-G,,7,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,07E8CF50-G,0,1,0 080D6720-G=M_Nod tib. refinery attack 4,073AE6C0-G,,8,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,07E8CDB0-G,0,1,1 080D6620-G=M_Nod upgrade center attack 1,084E03D0-G,,9,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CE380-G,0,1,0 080D6520-G=M_Nod upgrade center attack 2,084E0230-G,,5,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,080CE1C0-G,0,1,0 080D6420-G=M_Nod upgrade center attack 3,084E0090-G,,7,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084E0B90-G,0,1,0 080D6320-G=M_Nod upgrade center attack 4,084E0AC0-G,,8,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,084E09F0-G,0,1,0 080D6220-G=M_Nod vehicle attack 1,073A8070-G,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080D6120-G=M_Nod vehicle attack 2,073A9E50-G,,3,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0753E780-G,0,1,0 080D7F20-G=M_Nod vehicle attack 3,073A9D80-G,,8,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080D7E20-G=M_Nod vehicle attack 4,073AA970-G,,7,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,0753E5F0-G,0,1,0 080D7D20-G=M_Nod vehicle attack 5,073A95D0-G,,5,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080FB470-G=M_Nod vehicle attack 6,084D9710-G,,10,1,MHIJACK,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 09F2EB20-G=M_Nod banshee pool,0B7D67E0-G,,9,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 09F2EA20-G=M_Nod base repair vehicle pool,07E7F0E0-G,,7,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 09F2E920-G=M_Nod cyborg commando pool,084D7830-G,,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080D7820-G=M_Nod cyborg pool,084DAA80-G,,4,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 080D7720-G=M_Nod devil's tongue pool,07ECE560-G,,7,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080D7620-G=M_Nod engineer pool,084DF2C0-G,,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 09F2E520-G=M_Nod harpy pool,0B7D6700-G,,5,1,NAHPAD,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 09A92A20-G=M_Nod light infantry pool,073AB9B0-G,,1,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 09F2E320-G=M_Nod mutant hijacker pool,084DF120-G,,10,1,NATMPL,0100000003000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080E8F20-G=M_Nod ranged pool ,0832E300-G,,6,1,NARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080E8E20-G=M_Nod recon pool 1,0832E230-G,,5,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080E8D20-G=M_Nod recon pool 2,073AF270-G,,2,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 080E8C20-G=M_Nod rocket infantry pool,074A32F0-G,,2,1,NAHAND,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 080E8B20-G=M_Nod stealth pool,0832E160-G,,8,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080E8A20-G=M_Nod subterranean APC pool,073AF400-G,,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,,0,1,0 080E8920-G=M_Nod tank pool,0832E3D0-G,,3,1,NAWEAP,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 07F3D830-G=E_GDI APC/infantry attack,07F3DAD0-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,07F3DAD0-G,1,0,0 080D8920-G=M_GDI APC/infantry attack,07F3D930-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 07F3EF20-G=H_GDI APC/infantry attack,07F3D930-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,07F3D930-G,0,0,1 07F3EE20-G=E_Nod APC/infantry attack,07F3C110-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,70.000000,1,0,2,0,07F3C110-G,1,0,0 080D8620-G=M_Nod APC/infantry attack,07F3DA00-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,,0,1,0 07F3EC20-G=H_Nod APC/infantry attack,07F3DA00-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,07F3DA00-G,0,0,1 08507720-G=E_GDI replace MCV,084ECBA0-G,,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,1,0,0 08B80760-G=H_GDI replace MCV,084EC7F0-G,,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,,0,0,1 08B80660-G=M_GDI replace MCV,084EC7F0-G,,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,,0,1,0 091F4720-G=E_Nod replace MCV,084EC720-G,,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,1,0,0 0930BDC0-G=H_Nod replace MCV,084EC650-G,,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,,0,0,1 0930A240-G=M_Nod replace MCV,084EC650-G,,10,1,GACNST,0100000005000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,,0,1,0 0AA4F100-G=E_GDI infantry attack 6,080DA100-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,1,0,080DA100-G,1,0,0 0AA50F20-G=H_GDI infantry attack 6,0AA4F200-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,0AA4F200-G,0,0,1 08BBE9C0-G=E_GDI factories attack 5,080DBF50-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBF50-G,1,0,0 08BBE8C0-G=E_GDI factories attack 6,080DBE80-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBE80-G,1,0,0 08BBCF20-G=H_GDI factories attack 5,08BBE7F0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBE7F0-G,0,0,1 08BBCE20-G=H_GDI factories attack 6,08BBE720-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBE720-G,0,0,1 08BBCB80-G=E_GDI vehicle attack 8,080DBC10-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBC10-G,1,0,0 08BBCA80-G=E_GDI vehicle attack 9,080DBB40-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,080DBB40-G,1,0,0 080D9520-G=M_GDI infantry attack 6,0AA4F200-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,,0,1,0 080D9420-G=M_GDI factories attack 5,08BBE7F0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,0,0,,0,1,0 080D9320-G=M_GDI factories attack 6,08BBE720-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 08BBC4E0-G=H_GDI vehicle attack 8,08BBC6B0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBC6B0-G,0,0,1 08BBC3E0-G=H_GDI vehicle attack 9,08BBC5E0-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08BBC5E0-G,0,0,1 080DAF20-G=M_GDI vehicle attack 8,08BBC6B0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 080DAE20-G=M_GDI vehicle attack 9,08BBC5E0-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 0AA34CA0-G=E_Nod factories attack 5,080DB8D0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080DB8D0-G,1,0,0 0AA34BA0-G=E_Nod factories attack 6,080DB800-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,080DB800-G,1,0,0 0AA34900-G=H_Nod factories attack 5,0AA34AD0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA34AD0-G,0,0,1 0AA34800-G=H_Nod factories attack 6,0AA34A00-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA34A00-G,0,0,1 080DA920-G=M_Nod factories attack 5,0AA34AD0-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080DA820-G=M_Nod factories attack 6,0AA34A00-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 0AA34360-G=E_Nod infantry attack 5,080DB590-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080DB590-G,1,0,0 0AA34260-G=E_Nod infantry attack 6,080DB4C0-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,080DB4C0-G,1,0,0 0AA35F20-G=H_Nod infantry attack 5,0AA34190-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA34190-G,0,0,1 0AA35E20-G=H_Nod infantry attack 6,0AA340C0-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA340C0-G,0,0,1 080DA320-G=M_Nod infantry attack 5,0AA34190-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 080DA220-G=M_Nod infantry attack 6,0AA340C0-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 0AA35980-G=E_Nod vehicle attack 7,0AA35B50-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA35B50-G,1,0,0 0AA35070-G=E_Nod vehicle attack 8,0AA35A80-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0AA35A80-G,1,0,0 0AA36D80-G=H_Nod vehicle attack 7,0AA36F50-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA36F50-G,0,0,1 0AA36C80-G=H_Nod vehicle attack 8,0AA36E80-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0AA36E80-G,0,0,1 080DBC20-G=M_Nod vehicle attack 7,0AA36F50-G,,1,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 080DBB20-G=M_Nod vehicle attack 8,0AA36E80-G,,2,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 09EEBCD0-G=M_GDI base air defense 3,084E4F50-G,,7,0,APACHE,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E38B0-G,0,1,0 08440AE0-G=E_GDI base air defense 5,084E5B50-G,,7,0,JUMPJET,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,084E5A80-G,1,0,0 08440950-G=E_Nod base air defense 5,084E8670-G,,8,0,JUMPJET,0500000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,084E85A0-G,1,0,0 08440850-G=M_GDI base air defense 5,084E4F50-G,,7,0,JUMPJET,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,084E38B0-G,0,1,0 08440750-G=M_Nod base air defense 5,084E3130-G,,8,0,JUMPJET,0400000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,084E3060-G,0,1,0 08440650-G=H_GDI base air defense 5,084E4F50-G,,7,0,JUMPJET,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,1,0,084E38B0-G,0,0,1 08440550-G=H_Nod base air defense 5,084E3130-G,,8,0,JUMPJET,0300000003000000000000000000000000000000000000000000000000000000,70.000000,70.000000,70.000000,1,0,2,0,084E3060-G,0,0,1 [Digest] 1=aG6xg9lMqJoMyUqKRzv4QJZ2vmk= ================================================ FILE: DXMainClient/Resources/INI/aifs.ini ================================================ [TaskForces] 1000=08820130-G 1001=08822830-G 1002=0A70DB10-G 1003=0A70E6A0-G 1004=0832C3F0-G 1005=07ECC200-G 1006=08063D20-G 1007=08063DE0-G 1008=0804C6C0-G 1009=0804C530-G 1010=073A9510-G 1011=073A9CC0-G 1012=07ECD1F0-G 1013=075BFDC0-G 1014=0859E2E0-G 1015=07EA4290-G 1016=073A8CF0-G 1017=07ECE4A0-G 1018=084B2F60-G 1019=07ECFB60-G 1020=096473E0-G 1021=08603140-G 1022=07EA1E90-G 1023=08602820-G 1024=0860DE90-G 1025=07ED0860-G 1026=07ED0460-G 1027=08050A00-G 1028=07ED1330-G 1029=073AACA0-G 1030=075E02F0-G 1031=09F7B380-G 1032=073AA3F0-G 1033=075E2310-G 1034=075E2180-G 1035=07E04DC0-G 1036=085EC2D0-G 1037=09EF0540-G 1038=09EF2BB0-G 1039=09F11CE0-G 1040=08600780-G 1041=0965B600-G 1042=0965D960-G 1043=08600DC0-G 1044=08601B60-G 1045=086019D0-G 1046=08601770-G 1047=086015E0-G 1048=08601380-G 1049=0846A4D0-G 1050=08602B40-G 1051=086029B0-G 1052=08602640-G 1053=097246D0-G 1054=08602DC0-G 1055=086039B0-G 1056=0805DAC0-G 1057=08060740-G 1058=07F3CAE0-G 1059=07F3A490-G 1060=0860B440-G [08820130-G] Name=1 juggernaut 0=1,JUGG Group=-1 [08822830-G] Name=3 juggernauts 0=3,JUGG Group=-1 [0A70DB10-G] Name=1 cyborg reaper 0=1,REAPER Group=-1 [0A70E6A0-G] Name=3 cyborg reapers 0=3,REAPER Group=-1 [0832C3F0-G] Name=1 amphibious APC, 5 engineers 0=5,ENGINEER 1=1,APC Group=-1 [07ECC200-G] Name=1 subterranean APC, 5 engineers 0=1,SAPC 1=5,ENGINEER Group=-1 [08063D20-G] Name=1 subterranean APC, 3 eng, 2 roc 0=1,SAPC 1=3,ENGINEER 2=2,E3 Group=-1 [08063DE0-G] Name=1 amphibious APC,3 eng, 2 disc 0=1,APC 1=3,ENGINEER 2=2,E2 Group=-1 [0804C6C0-G] Name=1 amphibious APC,1 ghost,4 disc 0=1,GHOST 1=1,APC 2=4,E2 Group=-1 [0804C530-G] Name=1 sub. APC, 1 cyb com, 4 cyborgs 0=4,CYBORG 1=1,SAPC 2=1,CYC2 Group=-1 [073A9510-G] Name=4 attack cycles 0=4,BIKE Group=-1 [073A9CC0-G] Name=4 stealth tanks 0=4,STNK Group=-1 [07ECD1F0-G] Name=3 wolverines 0=3,SMECH Group=-1 [075BFDC0-G] Name=3 hover MLRS 0=3,HVR Group=-1 [0859E2E0-G] Name=1 mobile repair vehicle 0=1,REPAIR Group=-1 [07EA4290-G] Name=3 artillery 0=3,ART2 Group=-1 [073A8CF0-G] Name=4 tick tanks 0=4,TTNK Group=-1 [07ECE4A0-G] Name=4 devil's tongue 0=4,SUBTANK Group=-1 [084B2F60-G] Name=1 amphibious APC 0=1,APC Group=-1 [07ECFB60-G] Name=3 titans 0=3,MMCH Group=-1 [096473E0-G] Name=3 disruptors 0=3,SONIC Group=-1 [08603140-G] Name=1 ORCA fighters 0=1,ORCA Group=-1 [07EA1E90-G] Name=1 ORCA bombers 0=1,ORCAB Group=-1 [08602820-G] Name=1 harpies 0=1,APACHE Group=-1 [0860DE90-G] Name=1 banshee 0=1,SCRIN Group=-1 [07ED0860-G] Name=4 titans 0=4,MMCH Group=-1 [07ED0460-G] Name=1 mammoth mk. II 0=1,HMEC Group=-1 [08050A00-G] Name=1 sub APC, 1 hijacker, 4 rocket 0=1,MHIJACK 1=1,SAPC 2=4,E3 Group=-1 [07ED1330-G] Name=4 wolverines 0=4,SMECH Group=-1 [073AACA0-G] Name=4 attack buggies 0=4,BGGY Group=-1 [075E02F0-G] Name=4 disc throwers 0=4,E2 Group=-1 [09F7B380-G] Name=3 jumpjet infantry 0=3,JUMPJET Group=-1 [073AA3F0-G] Name=1 mutant hijacker 0=1,MHIJACK Group=-1 [075E2310-G] Name=4 rocket infantry 0=4,E3 Group=-1 [075E2180-G] Name=4 cyborgs 0=4,CYBORG Group=-1 [07E04DC0-G] Name=4 light infantry 0=4,E1 Group=-1 [085EC2D0-G] Name=1 subterranean APC 0=1,SAPC Group=-1 [09EF0540-G] Name=3 engineers 0=3,ENGINEER Group=-1 [09EF2BB0-G] Name=1 ghoststalker 0=1,GHOST Group=-1 [09F11CE0-G] Name=1 cyborg commando 0=1,CYC2 Group=-1 [08600780-G] Name=1 titans 0=1,MMCH Group=-1 [0965B600-G] Name=1 hover MLRS 0=1,HVR Group=-1 [0965D960-G] Name=1 disruptor 0=1,SONIC Group=-1 [08600DC0-G] Name=1 disc throwers 0=1,E2 Group=-1 [08601B60-G] Name=1 jumpjet infantry 0=1,JUMPJET Group=-1 [086019D0-G] Name=1 tick tanks 0=1,TTNK Group=-1 [08601770-G] Name=1 stealth tanks 0=1,STNK Group=-1 [086015E0-G] Name=1 rocket infantry 0=1,E3 Group=-1 [08601380-G] Name=1 cyborgs 0=1,CYBORG Group=-1 [0846A4D0-G] Name=1 attack cycles 0=1,BIKE Group=-1 [08602B40-G] Name=1 devil's tongue 0=1,SUBTANK Group=-1 [086029B0-G] Name=1 engineers 0=1,ENGINEER Group=-1 [08602640-G] Name=1 light infantry 0=1,E1 Group=-1 [097246D0-G] Name=1 artillery 0=1,ART2 Group=-1 [08602DC0-G] Name=1 attack buggies 0=1,BGGY Group=-1 [086039B0-G] Name=1 wolverines 0=1,SMECH Group=-1 [0805DAC0-G] Name=1 amph APC,1 eng, 2 lt. 2 disc 0=1,ENGINEER 1=1,APC 2=2,E2 3=2,E1 Group=-1 [08060740-G] Name=1 sub APC, 1 eng, 2 rocket, 2 lt 0=1,ENGINEER 1=1,SAPC 2=2,E1 3=2,E3 Group=-1 [07F3CAE0-G] Name=1 sub. APC, 3 lt, 2 cyborgs 0=2,CYBORG 1=3,E1 2=1,SAPC Group=-1 [07F3A490-G] Name=1 amphibious APC, 3 lt, 2 disc 0=1,APC 1=2,E2 2=3,E1 Group=-1 [0860B440-G] Name=1 MCV 0=1,MCV Group=-1 [ScriptTypes] 1000=07F7C5E0-G 1001=075AD760-G 1002=08462780-G 1003=0786DA60-G 1004=088DDE00-G 1005=08463030-G 1006=075ABE00-G 1007=07397BE0-G 1008=07E686F0-G 1009=085F3E00-G 1010=085F3980-G 1011=075A3070-G 1012=07F7B2A0-G 1013=07F7D0D0-G 1014=07F7E3B0-G 1015=0960AAA0-G 1016=07F76BE0-G 1017=07F3DE00-G 1018=08B50140-G [07F7C5E0-G] Name=Base defense attack 0=0,7 1=0,1 [075AD760-G] Name=Construction yard attack 0=46,131084 1=49,0 2=0,1 [08462780-G] Name=Factories attack 0=0,6 1=0,1 [0786DA60-G] Name=Deployed base defense 0=9,0 1=11,10 [088DDE00-G] Name=Missile silo attack 0=46,131113 1=49,0 2=0,1 [08463030-G] Name=Power facilities attack 0=0,9 1=0,1 [075ABE00-G] Name=Tiberium refinery attack 0=46,131073 1=49,0 2=0,1 [07397BE0-G] Name=Upgrade center attack 0=46,131076 1=49,0 2=0,1 [07E686F0-G] Name=Base defense 0=11,10 [085F3E00-G] Name=APC/engineer attack 0=14,0 1=43,0 2=47,131084 3=49,0 4=8,2 5=11,14 [085F3980-G] Name=APC/eng. steal money 0=14,0 1=43,0 2=47,131073 3=49,0 4=8,2 5=46,131073 6=46,131074 7=11,14 [075A3070-G] Name=APC/commando attack 0=14,0 1=47,131084 2=49,0 3=8,2 4=0,2 5=0,1 [07F7B2A0-G] Name=Harvester attack 0=0,3 1=0,1 [07F7D0D0-G] Name=Aerial base attack 0=0,2 1=0,1 [07F7E3B0-G] Name=Vehicle attack 0=0,5 1=0,1 [0960AAA0-G] Name=APC/thief steal vehicles 0=14,0 1=47,1 2=49,0 3=8,2 4=0,5 [07F76BE0-G] Name=Infantry attack 0=0,4 1=0,1 [07F3DE00-G] Name=APC/infantry attack 0=14,0 1=43,0 2=47,12 3=49,0 4=8,2 5=0,1 [08B50140-G] Name=Replace MCV 0=9,0 [TeamTypes] 1000=08820200-G 1001=08824090-G 1002=08822750-G 1003=08D1B060-G 1004=09E4A280-G 1005=09E4A4F0-G 1006=09E4FF40-G 1007=09E4FE60-G 1008=09E4FD80-G 1009=041A7830-G 1010=041A7750-G 1011=041A7670-G 1012=041A7590-G 1013=041A74B0-G 1014=07CC69F0-G 1015=07CC6910-G 1016=0A70DBE0-G 1017=0A70E770-G 1018=0A70E140-G 1019=0A70E060-G 1020=0A70FC10-G 1021=0A70FB30-G 1022=0A70F720-G 1023=0A70F640-G 1024=095E1D80-G 1025=095E1CA0-G 1026=08823D20-G 1027=08823C40-G 1028=08822150-G 1029=095E1920-G 1030=08823930-G 1031=08823850-G [08820200-G] Name=E_GDI base defense attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=08820130-G [08824090-G] Name=H_GDI base defense attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=08822830-G [08822750-G] Name=E_GDI con. yard attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08820130-G [08D1B060-G] Name=H_GDI con. yard attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=08822830-G [09E4A280-G] Name=E_GDI factories attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08820130-G [09E4A4F0-G] Name=H_GDI factories attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=08822830-G [09E4FF40-G] Name=E_GDI juggernaut pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=0786DA60-G TaskForce=08820130-G [09E4FE60-G] Name=H_GDI juggernaut pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=0786DA60-G TaskForce=08822830-G [09E4FD80-G] Name=E_GDI missile silo attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08820130-G [041A7830-G] Name=H_GDI missile silo attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=08822830-G [041A7750-G] Name=E_GDI power facilities attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08820130-G [041A7670-G] Name=H_GDI power facilities attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=08822830-G [041A7590-G] Name=E_GDI tib. refinery attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08820130-G [041A74B0-G] Name=H_GDI tib. refinery attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=08822830-G [07CC69F0-G] Name=E_GDI upgrade center attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08820130-G [07CC6910-G] Name=H_GDI upgrade center attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=GDI Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=08822830-G [0A70DBE0-G] Name=E_Nod cyborg reaper pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0A70DB10-G [0A70E770-G] Name=H_Nod cyborg reaper pool VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=yes LooseRecruit=no Aggressive=no Suicide=no Priority=4 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=yes OnlyTargetHouseEnemy=yes Script=07E686F0-G TaskForce=0A70E6A0-G [0A70E140-G] Name=E_Nod con. yard attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0A70DB10-G [0A70E060-G] Name=H_Nod con. yard attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075AD760-G TaskForce=0A70E6A0-G [0A70FC10-G] Name=E_Nod factories attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0A70DB10-G [0A70FB30-G] Name=H_Nod factories attack 7 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08462780-G TaskForce=0A70E6A0-G [0A70F720-G] Name=E_Nod missile silo attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0A70DB10-G [0A70F640-G] Name=H_Nod missile silo attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=088DDE00-G TaskForce=0A70E6A0-G [095E1D80-G] Name=E_Nod power facilities attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0A70DB10-G [095E1CA0-G] Name=H_Nod power facilities attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=12 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=08463030-G TaskForce=0A70E6A0-G [08823D20-G] Name=E_Nod tib. refinery attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0A70DB10-G [08823C40-G] Name=H_Nod tib. refinery attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=075ABE00-G TaskForce=0A70E6A0-G [08822150-G] Name=E_Nod upgrade center attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0A70DB10-G [095E1920-G] Name=H_Nod upgrade center attack 5 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07397BE0-G TaskForce=0A70E6A0-G [08823930-G] Name=E_Nod base defense attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=no Suicide=yes Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=0A70DB10-G [08823850-G] Name=H_Nod base defense attack 3 VeteranLevel=1 Loadable=no Full=no Annoyance=no GuardSlower=no House=Nod Recruiter=no Autocreate=no Prebuild=no Reinforce=no Droppod=no Whiner=no LooseRecruit=no Aggressive=yes Suicide=no Priority=8 Max=2 TechLevel=0 Group=-1 OnTransOnly=no AvoidThreats=no IonImmune=no TransportsReturnOnUnload=no AreTeamMembersRecruitable=yes IsBaseDefense=no OnlyTargetHouseEnemy=yes Script=07F7C5E0-G TaskForce=0A70E6A0-G [AITriggerTypes] 07CC6F10-G=E_GDI juggernaut pool,09E4FF40-G,,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,09E4FF40-G,1,0,0 07CC6E00-G=M_GDI juggernaut pool,09E4FE60-G,,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 07CC6CF0-G=H_GDI juggernaut pool,09E4FE60-G,,6,1,GARADR,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,09E4FE60-G,0,0,1 07CC6580-G=E_GDI base defense attack 3,08820200-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,08820200-G,1,0,0 07CC6470-G=M_GDI base defense attack 3,08824090-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 07CC6360-G=H_GDI base defense attack 3,08824090-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,08824090-G,0,0,1 07CC6250-G=E_GDI con. yard attack 5,08822750-G,,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,08822750-G,1,0,0 07CC6140-G=M_GDI con. yard attack 5,08D1B060-G,,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 07CC6030-G=H_GDI con. yard attack 5,08D1B060-G,,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,08D1B060-G,0,0,1 07CC7F10-G=E_GDI factories attack 7,09E4A280-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,09E4A280-G,1,0,0 07CC7E00-G=M_GDI factories attack 7,09E4A4F0-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,,0,1,0 07CC7CF0-G=H_GDI factories attack 7,09E4A4F0-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,1,0,09E4A4F0-G,0,0,1 07CC7BE0-G=E_GDI missile silo attack 5,09E4FD80-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,09E4FD80-G,1,0,0 07CC7AD0-G=M_GDI missile silo attack 5,041A7830-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,,0,1,0 07CC79C0-G=H_GDI missile silo attack 5,041A7830-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,041A7830-G,0,0,1 07CC78B0-G=E_GDI power facilities attack 5,041A7750-G,,6,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,1,0,041A7750-G,1,0,0 07CC77A0-G=M_GDI power facilities attack 5,041A7670-G,,6,3,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,1,0,,0,1,0 07CC7690-G=H_GDI power facilities attack 5,041A7670-G,,6,3,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,1,0,041A7670-G,0,0,1 041CFAE0-G=E_GDI tib. refinery attack 5,041A7590-G,,6,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,1,0,041A7590-G,1,0,0 07CC6BE0-G=M_GDI tib. refinery attack 5,041A74B0-G,,6,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,1,0,,0,1,0 07CC6AD0-G=H_GDI tib. refinery attack 5,041A74B0-G,,6,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,041A74B0-G,0,0,1 07CC6800-G=E_GDI upgrade center attack 5,07CC69F0-G,,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,1,0,07CC69F0-G,1,0,0 07CC66F0-G=M_GDI upgrade center attack 5,07CC6910-G,,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,1,0,,0,1,0 08850F10-G=H_GDI upgrade center attack 5,07CC6910-G,,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,1,0,07CC6910-G,0,0,1 0A70E440-G=E_Nod cyborg reaper pool,0A70DBE0-G,,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A70DBE0-G,1,0,0 0A70E330-G=M_Nod cyborg reaper pool,0A70E770-G,,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 0A70E220-G=H_Nod cyborg reaper pool,0A70E770-G,,6,1,NATECH,0100000003000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0A70E770-G,0,0,1 0A70FF10-G=E_Nod con. yard attack 5,0A70E140-G,,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,0A70E140-G,1,0,0 0A70FE00-G=M_Nod con. yard attack 5,0A70E060-G,,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,,0,1,0 0A70FCF0-G=H_Nod con. yard attack 5,0A70E060-G,,6,0,GACNST,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0A70E060-G,0,0,1 0A70FA20-G=E_Nod factories attack 7,0A70FC10-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,0A70FC10-G,1,0,0 0A70F910-G=M_Nod factories attack 7,0A70FB30-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 0A70F800-G=H_Nod factories attack 7,0A70FB30-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,0A70FB30-G,0,0,1 0A70F530-G=E_Nod missile silo attack 5,0A70F720-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,0A70F720-G,1,0,0 0A70F420-G=M_Nod missile silo attack 5,0A70F640-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,,0,1,0 0A70F310-G=H_Nod missile silo attack 5,0A70F640-G,,6,0,NAMISL,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,0A70F640-G,0,0,1 0A70F040-G=E_Nod power facilities attack 5,095E1D80-G,,6,3,,0000000000000000000000000000000000000000000000000000000000000000,20.000000,20.000000,20.000000,1,0,2,0,095E1D80-G,1,0,0 07CDB770-G=M_Nod power facilities attack 5,095E1CA0-G,,6,3,,0000000000000000000000000000000000000000000000000000000000000000,30.000000,30.000000,30.000000,1,0,2,0,,0,1,0 08823F10-G=H_Nod power facilities attack 5,095E1CA0-G,,6,3,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,095E1CA0-G,0,0,1 08821F10-G=E_Nod upgrade center attack 5,08822150-G,,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,08822150-G,1,0,0 08821E00-G=M_Nod upgrade center attack 5,095E1920-G,,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,50.000000,10.000000,70.000000,1,0,2,0,,0,1,0 08821CF0-G=H_Nod upgrade center attack 5,095E1920-G,,6,0,GAPLUG,0100000003000000000000000000000000000000000000000000000000000000,60.000000,10.000000,70.000000,1,0,2,0,095E1920-G,0,0,1 08823740-G=E_Nod base defense attack 3,08823930-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,40.000000,40.000000,40.000000,1,0,2,0,08823930-G,1,0,0 08823630-G=M_Nod base defense attack 3,08823850-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,50.000000,50.000000,50.000000,1,0,2,0,,0,1,0 08823520-G=H_Nod base defense attack 3,08823850-G,,6,-1,,0000000000000000000000000000000000000000000000000000000000000000,60.000000,60.000000,60.000000,1,0,2,0,08823850-G,0,0,1 08860D10-G=E_Nod tib. refinery attack 5,08823D20-G,,6,0,PROC,0400000003000000000000000000000000000000000000000000000000000000,20.000000,10.000000,70.000000,1,0,2,0,08823D20-G,1,0,0 08860C00-G=M_Nod tib. refinery attack 5,08823C40-G,,6,0,PROC,0300000003000000000000000000000000000000000000000000000000000000,30.000000,10.000000,70.000000,1,0,2,0,,0,1,0 08860AF0-G=H_Nod tib. refinery attack 5,08823C40-G,,6,0,PROC,0200000003000000000000000000000000000000000000000000000000000000,40.000000,10.000000,70.000000,1,0,2,0,08823C40-G,0,0,1 [Digest] 1=VIYs66hN8BSm544VkgK0QVZgCtw= ================================================ FILE: DXMainClient/Resources/INI/art.ini ================================================ ; ART.INI ; This control file provides the information necessary to handle the ; artwork in Tiberian Sun. The information usually covers sprite ; animation and related characteristics. There is one section ; for each sprite data file. The sprite section names are unique ; within the system and are referred to by name in the RULES.INI ; control file. ; *** Game Object Art *** ; Cameo = image to use if this object happens to appear in the sidebar (def=none) ; Voxel = Is this a voxel image (def=no)? ; Remapable = Can this object be remapped to owner's color (def=no)? ; Normalized = If its animation be regulated to appear constant speed (def=no)? ; Theater = Does it have theater specific imagery (def=no)? ; NewTheater = Does it have theater specific name (def=no)? This changes the second character of the name dependant on theater. ; RotCount = number of rotation stages [old system only] (def=32)? ; ShadowIndex = index of voxel piece to use for the shadow (def=0) [only needed for voxel hierarchies] ; TurretOffset = turret center offset along body centerline (def=0) ; FireAngle = default angle to raise barrel above direct line to targe [in degrees] (def=10) ; BarrelLength = length of barrel expressed in 1.5 inch increments (def = 0 i.e., no independant barrel equipped) ; BarrelOffset = barrel pivot point [distance from center-base along X (horz) axis and Y (vert) axis] ; PrimaryFireFLH = lepton offset [Forward,Lateral,Height from turret center] for bullet start position (def=0,0,0) ; SecondaryFireFLH = alternate weapon offset for bullet start position (def=0,0,0) ; <<< applies only to artwork used for infantry >>> ; Sequence = infantry animation sequence name [required] ; Crawls = Does the infantry have crawling animation [else it is running] (def=yes)? ; FireUp = frame of projectile launch when firing standing [required if it has firing animation] (def=0) ; <<< applies only to vehicles >>> ; VisibleLoad = Does the unit have duplicate shape set for loaded with ammo state (def=no)? ; UseTurretShadow = use the turret of the object to cast shadow (def=no)? ; <<< applies only to building types >>> ; Foundation = the size of the building [width x height] (def=1x1) ; Height = height of the building [in levels] ; PrimaryFirePixelOffset = Pixel offset to apply to building when firing a bullet. Used when the building has no turret and so the offset is fixed. ; SecondaryFirePixelOffset = Pixel offset to apply to building when firing a bullet. Used when the building has no turret and so the offset is fixed. ; SimpleDamage = Does building have simple damage imagery (def=yes)? ; Buildup = graphic image to use when construction occurs (def=none) ; AuxAnim = Anim to use for overlaying animation states. ; AltImage = Is there an alternate image [frame #2] to use when viewed by enemy player (def=no)? ; ChargeAnim = Does this building have Tesla-coil like charge up anim (def=no)? ; SiloDamage = Is damage image based on base Tiberium storage level (def=no)? ; Flat = Is building flat on the ground [helps with drawing logic to know this] (def=no)? ; Recoilless = Does the building NOT have recoil anim even though it might have a turret (def=no)? ; ToOverlay = when placed down, actually convert into this overlay type (def=none) ; DamageLevels = how many levels of damage it can take [for walls only] ; PowerUp1Anim = The animation to add to this building when powered up by one level ; PowerUp1AnimDamaged = Damaged version of The animation to add to this building when powered up by one level ; PowerUp1LocX = The x offset from the buildings draw position for this powerup animation ; PowerUp1LocY = The x offset from the buildings draw position for this powerup animation ; PowerUp1LocZ = Adjustment to normal Z when rendering this animation. This could be used to make the animation appear behind the building. ; PowerUp1YSort= Amount to add to anims sorting position so that it renders in the correct order relative to other objects ; PowerUp2Anim = The animation to add to this building (in addition to the level 1 power up) when powered up by two levels ; PowerUp2AnimDamaged = Damaged version of The animation to add to this building when powered up by two level ; PowerUp2LocX = The x offset from the buildings draw position for this powerup animation ; PowerUp2LocY = The x offset from the buildings draw position for this powerup animation ; PowerUp2LocZ = Adjustment to normal Z when rendering this animation. This could be used to make the animation appear behind the building. ; PowerUp2YSort= Amount to add to anims sorting position so that it renders in the correct order relative to other objects ; PowerUp3Anim = The animation to add to this building (in addition to the level 1&2 power ups) when powered up by three levels ; PowerUp3AnimDamaged = Damaged version of The animation to add to this building when powered up by three level ; PowerUp3LocX = The x offset from the buildings draw position for this powerup animation ; PowerUp3LocY = The x offset from the buildings draw position for this powerup animation ; PowerUp3LocZ = Adjustment to normal Z when rendering this animation. This could be used to make the animation appear behind the building. ; PowerUp3YSort= Amount to add to anims sorting position so that it renders in the correct order relative to other objects ; ActiveAnim = Animation to use for building active animation ; ActiveAnimDamaged = Animation to use for building active animation when damaged ; ActiveAnimX = X offset from building position for active animation ; ActiveAnimY = Y offset from building position for active animation ; ActiveAnimYSort = Adjustment to position to use when sorting the layer prior to rendering. ; ActiveAnimZAdjust = Adjustment to normal Z when rendering this animation over the building. ; ActiveAnimPowered = Does the animation require that the building has power (def=yes) ; ActiveAnimTwo = Animation to use for building active animation ; ActiveAnimTwoDamaged = Animation to use for building active animation when damaged ; ActiveAnimTwoX = X offset from building position for active animation ; ActiveAnimTwoY = Y offset from building position for active animation ; ActiveAnimTwoYSort = Adjustment to position to use when sorting the layer prior to rendering. ; ActiveAnimTwoZAdjust = Adjustment to normal Z when rendering this animation over the building. ; ActiveAnimTwoPowered = Does the animation require that the building has power (def=yes) ; ActiveAnimThree = Animation to use for building active animation ; ActiveAnimThreeDamaged = Animation to use for building active animation when damaged ; ActiveAnimThreeX = X offset from building position for active animation ; ActiveAnimThreeY = Y offset from building position for active animation ; ActiveAnimThreeYSort = Adjustment to position to use when sorting the layer prior to rendering. ; ActiveAnimThreeZAdjust = Adjustment to normal Z when rendering this animation over the building. ; ActiveAnimThreePowered = Does the animation require that the building has power (def=yes) ; TerrainPalette = Draw this in the terrain palette not the building palette. (def=no) ; <<< applies on to vessels >>> ; Rotates = Does the vessel rotate [old system only] (def=yes)? ; <<< applies only to aircraft >>> ; Rotors = Does this aicraft have an attached rotor animation (def=no)? ; CustomRotor = Does it have custom rotor shapes according to facing (def=no)? [JUMPJET] Cameo=JJETICON Sequence=JumpjetSequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=100,0,120 [CYC2] Cameo=CYBCICON Sequence=CyborgSequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=100,-50,130 [CHAMSPY] Cameo=CHAMICON Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [E1] Cameo=E1ICON Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=80,0,85 [E2] Cameo=E2ICON Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=6 PrimaryFireFLH=60,0,100 FireProne=4 ; FireProne can be used to delay firing to sync with the firing animation [E3] Cameo=E4ICON Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=100,-25,135 ;[WEEDGUY] ;Cameo=WEATICON ;Sequence=WeedSequence ;Crawls=yes ;Remapable=yes ;FireUp=2 [MEDIC] Cameo=MEDIICON Sequence=MedicSequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=0,0,100 [GHOST] Sequence=E1Sequence Cameo=GOSTICON Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=100,0,100 [MHIJACK] Sequence=E1Sequence Cameo=CHAMICON Crawls=yes Remapable=yes FireUp=2 [SLAV] Sequence=E1Sequence Cameo=WEATICON Crawls=yes Remapable=yes FireUp=2 ; Nod Elite Cadre Soldier [ELCAD] Sequence=E1Sequence Cameo=WEATICON Crawls=yes Remapable=yes FireUp=2 [CYBORG] Cameo=CYBIICON Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=70,-30,120 FireProne=4 ; FireProne can be used to delay firing to sync with the firing animation/burst [MUTANT] Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [UMAGON] Sequence=E1Sequence Cameo=UMAGICON Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=100,0,100 [DOGGIE] Sequence=DoggieSequence Crawls=yes Remapable=yes FireUp=2 PrimaryFireFLH=0,0,100 [MWMN] Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [OXANNA] Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [TRATOS] Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [MUTANT3] Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [ENGINEER] Cameo=ENGNICON Sequence=E1Sequence Crawls=yes Remapable=yes FireUp=2 [CIV1] Sequence=E1Sequence Crawls=no FireUp=2 [CIV2] Sequence=E1Sequence Crawls=no FireUp=2 [CIV3] Sequence=E1Sequence Crawls=no FireUp=2 ; Vehicle artwork follows [TRUCKA] Voxel=yes Remapable=yes PrimaryFireFLH=40,32,96 SecondaryFireFLH=-32,80,120 PBarrelLength=192 [TRUCKB] Voxel=yes Remapable=yes PrimaryFireFLH=40,32,96 SecondaryFireFLH=-32,80,120 PBarrelLength=192 [4TNK] Voxel=yes Remapable=yes PrimaryFireFLH=40,32,96 SecondaryFireFLH=-32,80,120 PBarrelLength=192 [ART2] Cameo=ARTYICON Remapable=yes TurretOffset=-56 PBarrelLength=224 Voxel=yes PrimaryFireFLH=0,0,64 [WEED] Cameo=WEEDICON Voxel=yes Remapable=yes [HARV] Cameo=HARVICON Voxel=yes Remapable=yes [HORV] Voxel=yes Remapable=yes [REPAIR] Cameo=RBOTICON Voxel=yes Remapable=yes PrimaryFireFLH=110,0,105 [LPST] Cameo=LPSTICON Voxel=yes Remapable=yes [ICBM] Voxel=yes Remapable=yes [MCV] Cameo=MCVICON Remapable=yes Voxel=yes [HVR] Cameo=HOVRICON Voxel=yes TurretOffset=-64 Remapable=yes PrimaryFireFLH=64,32,128 [APC] Cameo=APCICON Voxel=yes Remapable=yes PrimaryFireFLH=0,48,48 [MMCH] Voxel=no Remapable=yes Cameo=MMCHICON PrimaryFireFLH=0,-50,100 PBarrelLength=250 SBarrelLength=250 TurretOffset=0 WalkFrames=15 [HMEC] Cameo=HMECICON Voxel=yes Remapable=yes PrimaryFireFLH=80,100,158 SecondaryFireFLH=-60,100,158 UseTurretShadow=yes ShadowIndex=12 [GGHUNT] Voxel=no Remapable=yes WalkFrames=8 [SMECH] Voxel=no Remapable=yes Cameo=SMCHICON PrimaryFireFLH=0,48,48 WalkFrames=12 FiringFrames=4 [TTNK] Cameo=TICKICON Voxel=yes Remapable=yes PrimaryFireFLH=0,0,100 PBarrelLength=136 TurretOffset=64 [BIKE] Cameo=CYCLICON Voxel=yes Remapable=yes PrimaryFireFLH=0,48,48 [BGGY] Cameo=BGGYICON Voxel=yes Remapable=yes PrimaryFireFLH=0,0,100 [SAPC] Cameo=SAPCICON Voxel=yes Remapable=yes PrimaryFireFLH=0,0,48 [STNK] Cameo=STNKICON Voxel=yes Remapable=yes PrimaryFireFLH=40,0,104 [SUBTANK] Cameo=SUBTICON Voxel=yes Remapable=yes PrimaryFireFLH=128,0,40 [SONIC] Cameo=SONIICON TurretOffset=-64 Voxel=yes Remapable=yes PrimaryFireFLH=0,0,150 [BUS] Voxel=yes [MONOCAR] Voxel=yes [CARGOCAR] Voxel=yes [MONOENG] Voxel=yes [PICK] Voxel=yes [CAR] Voxel=yes [WINI] Voxel=yes [1TNK] Voxel=yes Remapable=yes [2TNK] Voxel=yes Remapable=yes [3TNK] Voxel=yes Remapable=yes [ARTY] Voxel=yes Remapable=yes [HELI] Voxel=yes Remapable=yes [HIND] Voxel=yes Remapable=yes [JEEP] Voxel=yes Remapable=yes [M113] Voxel=yes Remapable=yes [MLRS] Voxel=yes Remapable=yes [MNLY] Voxel=yes Remapable=yes [MRJ] Voxel=yes Remapable=yes [TRAN] Voxel=yes Remapable=yes [TRUCK2] Voxel=yes Remapable=yes [TRUK] Voxel=yes Remapable=yes [UTNK] Voxel=yes Remapable=yes ; Aircraft artwork follows [ORCAB] Cameo=OBMBICON Voxel=yes PrimaryFireFLH=0,32,0 [ORCA] Cameo=ORCAICON Voxel=yes PrimaryFireFLH=0,32,0 [ORCATRAN] Cameo=CRRYICON Voxel=yes [TRNSPORT] Cameo=OTRNICON Voxel=yes [SCRIN] Cameo=PROICON Voxel=yes PrimaryFireFLH=0,32,0 [APACHE] Cameo=APCHICON Voxel=yes PrimaryFireFLH=0,32,0 [DPOD] Voxel=yes Remapable=yes PrimaryFireFLH=0,0,0 [DSHP] Voxel=yes Remapable=yes PrimaryFireFLH=0,0,0 ; firestorm addition [FTNK] Voxel=yes Remapable=yes PrimaryFireFLH=175,30,0 ; Building artwork follows [GATECH] Remapable=yes Normalized=yes Cameo=TECHICON Height=1 Foundation=3x2 Buildup=GATECHMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GATECH_A ActiveAnimZAdjust=-100 ActiveAnimDamaged=GATECH_AD [GAWEAP] Remapable=yes Cameo=WEAPICON Foundation=4x3 Height=2 NormalZAdjust=-10 AnimActive=0,1,0 Buildup=GAWEAPMK DemandLoadBuildup=true FreeBuildup=true DeployingAnim=GAWEAP_2 DoorAnim=GAWEAP_D DoorStages=9 UnderDoorAnim=GAWEAP_1 NewTheater=yes BibShape=GAWEAPBB ActiveAnim=GAWEAP_A ActiveAnimZAdjust=-119 ActiveAnimTwo=GAWEAP_B ActiveAnimTwoZAdjust=-119 ActiveAnimThree=GAWEAP_C ActiveAnimThreeZAdjust=-119 [NAWEAP] Remapable=yes Cameo=NWEPICON Foundation=4x3 Height=2 AnimActive=0,1,0 Buildup=NAWEAPMK DemandLoadBuildup=true FreeBuildup=true DeployingAnim=NAWEAP_2 DoorAnim=NAWEAP_B DoorStages=10 DamagedDoor=yes UnderDoorAnim=NAWEAP_1 NewTheater=yes BibShape=NAWEAPBB ProductionAnim=NAWEAP_A ProductionAnimX=0 ProductionAnimY=0 ProductionAnimYSort=0 ProductionAnimZAdjust=-119 ActiveAnim=NAWEAP_A ActiveAnimDamaged=NAWEAP_AD ActiveAnimZAdjust=-119 [GACNST] Remapable=yes Foundation=3x3 Height=1.5 AnimActive=0,26,3 Buildup=GACNSTMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GACNST_A ActiveAnimDamaged=GACNST_AD ActiveAnimZAdjust=-77 ActiveAnimTwo=GACNST_C ActiveAnimTwoDamaged=GACNST_CD ActiveAnimTwoZAdjust=-77 ActiveAnimThree=GACNST_B ActiveAnimThreeZAdjust=-27 PreProductionAnim=GACNST_B PreProductionAnimZAdjust=-15 ProductionAnim=GACNST_D ProductionAnimZAdjust=-25 [NAREFN] Remapable=yes Cameo=REFICON Foundation=4x3 Height=2 ZShapePointMove= 24, -12 Buildup=NAREFNMK DemandLoadBuildup=true FreeBuildup=true BibShape=NAREFNBB NewTheater=yes ActiveAnim=NAREFN_C ActiveAnimZAdjust=-100 ActiveAnimTwo=NAREFN_B ActiveAnimTwoZAdjust=-100 ActiveAnimTwoPowered=no PreProductionAnim=NAREFN_A ProductionAnim=NAREFN_AR PreProductionAnimX=-2 PreProductionAnimY=2 PreProductionAnimZAdjust=-100 ProductionAnimX=-2 ProductionAnimY=2 ProductionAnimZAdjust=-100 [PROC] Image=NAREFN Remapable=yes Cameo=REFICON Foundation=4x3 Height=2 ZShapePointMove= 24, -12 Buildup=NAREFNMK DemandLoadBuildup=true FreeBuildup=true BibShape=NAREFNBB NewTheater=yes ActiveAnim=NAREFN_C ActiveAnimZAdjust=-100 ActiveAnimTwo=NAREFN_B ActiveAnimTwoZAdjust=-100 ActiveAnimTwoPowered=no PreProductionAnim=NAREFN_A ProductionAnim=NAREFN_AR PreProductionAnimX=-2 PreProductionAnimY=2 PreProductionAnimZAdjust=-100 ProductionAnimX=-2 ProductionAnimY=2 ProductionAnimZAdjust=-100 [GASILO] Remapable=yes Cameo=SILOICON Foundation=2x2 Buildup=GASILOMK DemandLoadBuildup=true FreeBuildup=true SiloDamage=yes NewTheater=yes ActiveAnim=GASILO_B ActiveAnimDamaged=GASILO_BD ActiveAnimZAdjust=-100 SpecialAnim=GASILO_A SpecialAnimDamaged=GASILO_AD SpecialAnimZAdjust=-32 [GAHPAD] Remapable=yes Cameo=HELIICON Foundation=2x2 Height=1 Flat=yes Buildup=GAHPADMK BibShape=GAHPADBB DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GAHPAD_A ActiveAnimY=-12 ActiveAnimZAdjust=-32 ActiveAnimDamaged=GAHPAD_AD [NAHPAD] Remapable=yes Cameo=NHPDICON Foundation=2x2 Height=1.5 Flat=yes Buildup=NAHPADMK BibShape=NAHPADBB DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NAHPAD_A ActiveAnimZAdjust=-32 ActiveAnimDamaged=NAHPAD_AD [NARADR] Remapable=yes Normalized=yes Height=3 Cameo=NRADICON Foundation=2x2 Buildup=NARADRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NARADR_A ActiveAnimDamaged=NARADR_AD ActiveAnimZAdjust=-60 ActiveAnimYSort=100 [GAPLUG] Remapable=yes Normalized=yes Cameo=PLUGICON Foundation=2x3 Height=2 Buildup=GAPLUGMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnimZAdjust=-100 ActiveAnimTwoZAdjust=-125 ActiveAnim=GAPLUG_A ActiveAnimPowered=yes ActiveAnimTwo=GAPLUG_B ActiveAnimTwoDamaged=GAPLUG_BD ActiveAnimTwoPowered=no ActiveAnimTwoPoweredLight=yes ActiveAnimThree=GAPLUG_C ActiveAnimThreeZAdjust=-60 ActiveAnimThreePowered=no ActiveAnimThreePoweredLight=yes PowerUp1LocXX=0 PowerUp1LocYY=0 PowerUp1LocZZ=-30 PowerUp1YSort=-5 PowerUp2LocXX=-24 PowerUp2LocYY=-12 PowerUp2LocZZ=-42 PowerUp2YSort=50 [GARADR] Remapable=yes Normalized=yes Cameo=RADRICON Foundation=2x2 Height=4 Buildup=GARADRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GARADR_A ActiveAnimDamaged=GARADR_AD ActiveAnimZAdjust=-60 ActiveAnimPowered=yes [NASTLH] Cameo=CLCKICON Remapable=yes Normalized=yes Foundation=3x2 Buildup=NASTLHMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NASTLH_A ActiveAnimDamaged=NASTLH_AD ActiveAnimZAdjust=-40 [GAPOWR] Normalized=yes Remapable=yes Cameo=POWRICON Foundation=2x2 Buildup=GAPOWRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes Height=2 PowerUp1Anim=GAPOWR_B PowerUp1LocXX=-24 PowerUp1LocYY=13 PowerUp1LocZZ=-17 PowerUp1YSort=-5 ;PowerUp2Anim=GAPOWR_B ;PowerUp2LocXX=0 ;PowerUp2LocYY=0 ;PowerUp2LocZZ=-32 ;PowerUp2YSort=-5 PowerUp2Anim=GAPOWR_B PowerUp2LocXX=-48 PowerUp2LocYY=0 PowerUp2LocZZ=-32 PowerUp2YSort=-5 ActiveAnim=GAPOWR_A ActiveAnimDamaged=GAPOWR_AD ActiveAnimZAdjust=-100 ActiveAnimTwo=GAPOWR_B ActiveAnimTwoZAdjust=-32 ActiveAnimTwoYSort=-5 [NAPOWR] Normalized=yes Remapable=yes Cameo=NPWRICON Foundation=2x2 Height=2 Buildup=NAPOWRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NAPOWR_A ActiveAnimDamaged=NAPOWR_AD ActiveAnimZAdjust=-55 [NAAPWR] Normalized=yes Remapable=yes Cameo=APWRICON Foundation=2x3 Height=2 Buildup=NAAPWRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NAAPWR_A ActiveAnimDamaged=NAAPWR_AD ActiveAnimZAdjust=-55 [CAHOSP] Normalized=yes Remapable=no Foundation=3x4 Height=2 Buildup=CAHOSP NewTheater=yes ActiveAnim=CAHOSP_A ActiveAnimZAdjust=-100 DemandLoad=true [CAARMR] Normalized=yes Remapable=no Foundation=4x4 Height=1 Buildup=CAARMR NewTheater=yes ActiveAnim=CAARMR_A ActiveAnimZAdjust=-100 ActiveAnimPowered=no DemandLoad=true [CAPYR01] Normalized=yes Remapable=no Foundation=2x2 Height=2 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CAPYR02] Normalized=yes Remapable=no Foundation=4x4 Height=4 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CAPYR03] Normalized=yes Remapable=no Foundation=4x4 Height=4 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CACRSH01] Normalized=yes Remapable=no Foundation=1x1 Height=1 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CACRSH02] Normalized=yes Remapable=no Foundation=1x1 Height=1 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CACRSH03] Normalized=yes Remapable=no Foundation=1x1 Height=1 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CACRSH04] Normalized=yes Remapable=no Foundation=1x1 Height=1 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CACRSH05] Normalized=yes Remapable=no Foundation=1x1 Height=1 NewTheater=yes TerrainPalette=yes ExtraDamageStage=no DemandLoad=true [CAARAY] Cameo= Normalized=yes Remapable=no Foundation=2x2 Buildup= Height=3 NewTheater=yes ActiveAnim=CAARAY_A ActiveAnimZAdjust=-100 ActiveAnimTwo=CAARAY_B ActiveAnimTwoZAdjust=-100 ActiveAnimThree=CAARAY_C ActiveAnimThreeDamaged=CAARAY_CD ActiveAnimThreeZAdjust=-100 ActiveAnimFour=CAARAY_D ActiveAnimFourDamaged=CAARAY_DD ActiveAnimFourZAdjust=-100 DemandLoad=true [GASPOT] Cameo=SPOTICON Normalized=yes Remapable=yes Foundation=1x1 Buildup=GASPOTMK DemandLoadBuildup=true Height=3 NewTheater=yes ActiveAnim=GASPOT_A ActiveAnimDamaged=GASPOT_AD ActiveAnimZAdjust=-100 [GADPSA] Normalized=yes Remapable=yes Foundation=1x1 Buildup=GADPSAMK Height=2 NewTheater=yes ActiveAnim=GADPSA_A ActiveAnimZAdjust=-100 ExtraLight=-100 [GAICBM] Normalized=yes Remapable=yes Foundation=1x1 Buildup=GAICBMMK DemandLoadBuildup=true FreeBuildup=true Height=2 NewTheater=yes ActiveAnim=GAICBM_A ActiveAnimZAdjust=-100 ExtraLight=-100 [GATICK] Normalized=yes Remapable=yes Foundation=1x1 Buildup=GATICKMK Height=1 NewTheater=yes ExtraLight=-100 PrimaryFireFLH=48,0,64 PBarrelLength=136 [GAARTY] Normalized=yes Remapable=yes Foundation=1x1 Buildup=GAARTYMK Height=1 NewTheater=yes ExtraLight=350 PBarrelLength=224 PrimaryFireFLH=0,0,64 TurretNotExportedOnGround=yes [NATECH] Remapable=yes Normalized=yes Cameo=NTCHICON Foundation=2x2 Buildup=NATECHMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NATECH_A ActiveAnimZAdjust=-100 [NAHAND] Remapable=yes Normalized=yes Cameo=HANDICON Foundation=3x2 Buildup=NAHANDMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NAHAND_A ActiveAnimZAdjust=-100 ActiveAnimTwo=NAHAND_B ActiveAnimTwoDamaged=NAHAND_BD ActiveAnimTwoZAdjust=-100 [GAPILE] Remapable=yes Normalized=yes Cameo=BRRKICON Foundation=2x2 Buildup=GAPILEMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GAPILE_C ActiveAnimDamaged=GAPILE_CD ActiveAnimZAdjust=-39 ActiveAnimPowered=no ActiveAnimTwo=GAPILE_A ActiveAnimTwoZAdjust=-39 ActiveAnimThree=GAPILE_B ActiveAnimThreeDamaged=INVISO ;CAARAY_CD ActiveAnimThreeZAdjust=-75 [GADEPT] Remapable=yes Normalized=yes Cameo=FIXICON Foundation=3x3 AnimActive=0,7,2 Flat=yes Buildup=GADEPTMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes BibShape=GADEPTBB ActiveAnim=GADEPT_A ActiveAnimDamaged=GADEPT_AD ActiveAnimZAdjust=-100 ActiveAnimTwo=GADEPT_B ActiveAnimTwoZAdjust=-100 SpecialAnim=GADEPT_C1 SpecialAnimZAdjust=-100 SpecialAnimTwo=GADEPT_C2 SpecialAnimTwoZAdjust=-100 SpecialAnimThree=GADEPT_C3 SpecialAnimThreeZAdjust=-100 ProductionAnim=GADEPT_D ProductionAnimDamaged=GADEPT_DD ProductionAnimZAdjust=-100 [GAPAVE] Foundation=4x4 Cameo=PAVEICON ;IsNewTheater=yes [GAGREEN] ;Cameo=WALLICON Foundation=2x2 [GASAND] Cameo=SBAGICON Foundation=1x1 ToOverlay=GASAND DamageLevels=2 NewTheater=yes [GAWALL] Cameo=WALLICON Foundation=1x1 ToOverlay=GAWALL DamageLevels=3 ;2 in Firestorm NewTheater=yes [NAWALL] Cameo=NWALICON Foundation=1x1 ToOverlay=NAWALL DamageLevels=3 ;2 in Firestorm NewTheater=yes [CABHUT] Foundation=1x1 NewTheater=yes [CTDAM] Foundation=2x5 Height=5 ;NewTheater=no ActiveAnim=CTDAM_A ActiveAnimDamaged=CTDAM_AD ActiveAnimZAdjust=-120 ActiveAnimTwo=CTDAM_B ActiveAnimTwoDamaged=CTDAM_BD ActiveAnimTwoZAdjust=-60 TerrainPalette=yes ExtraDamageStage=false ;DemandLoad=true [UFO] Foundation=6x4 TerrainPalette=yes ExtraDamageStage=false DemandLoad=true Theater=yes Height=6 [AMMO01] Foundation=1x1 TerrainPalette=no ExtraDamageStage=false DemandLoad=false Theater=no Height=1 [NAPULS] Cameo=EMPICON Height=2 Remapable=yes Normalized=yes Foundation=2x2 PrimaryFireFLH=0,0,80 TurretNotExportedOnGround=yes PBarrelLength=110 Buildup=NAPULSMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes [GACTWR] Image=GACTWR Remapable=yes Normalized=yes Cameo=TOWRICON Foundation=1x1 Buildup=GACTWRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GACTWR_A ActiveAnimZAdjust=-19 Height=2 ;firestorm addition [GAGATE_A] Image=GAGATE_A Remapable=yes Cameo=GATEICON Foundation=3x1 Buildup=GAGATE_A SpecialZOverlay=GAGATEZA GateStages=9 NewTheater=yes [GAGATE_B] Image=GAGATE_B Remapable=yes Cameo=GAT2ICON Foundation=1x3 Buildup=GAGATE_B SpecialZOverlay=GAGATEZB GateStages=9 NewTheater=yes [NAGATE_A] Image=NAGATE_A Remapable=yes Cameo=NGATICON Foundation=3x1 Buildup=NAGATE_A SpecialZOverlay=NAGATEZA GateStages=6 NewTheater=yes [NAGATE_B] Image=NAGATE_B Remapable=yes Cameo=NGA2ICON Foundation=1x3 Buildup=NAGATE_B SpecialZOverlay=NAGATEZB GateStages=6 NewTheater=yes [GALITE] Cameo=LITEICON Image=GALITE Remapable=yes Foundation=1x1 TerrainPalette=yes NewTheater=yes ExtraDamageStage=false ;DemandLoad=yes ;Theater=yes [NATMPL] Image=NATMPL Cameo=TMPLICON Height=2 ;was 2.5 Foundation=4x3 Buildup=NATMPLMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NATMPL_A ActiveAnimZAdjust=-127 [NTPYRA] Image=NTPYRA Remapable=yes Foundation=4x4 NewTheater=yes Height=3 ActiveAnim=NTPYRA_A ActiveAnimDamaged=NTPYRA_AD ActiveAnimZAdjust=-30 DemandLoad=true [GAKODK] Image=GAKODK Remapable=yes Foundation=4x2 NewTheater=yes Height=1 ActiveAnim=GAKODK_A ActiveAnimDamaged=GAKODK_AD ActiveAnimZAdjust=-60 ActiveAnimTwo=GAKODK_B ActiveAnimTwoZAdjust=-127 ActiveAnimThree=GAKODK_C ActiveAnimThreeDamaged=GAKODK_CD ActiveAnimThreeZAdjust=-127 DemandLoad=true [NAMNTK] Image=NAMNTK Remapable=yes Foundation=1x3 NewTheater=yes Height=2 ;was 1.5 ActiveAnim=NAMNTK_A ActiveAnimZAdjust=-60 DemandLoad=true [GAFSDF_A] Image=GAFSDF_A ;Remapable=yes NewTheater=yes LoopStart=0 LoopEnd=4 LoopCount=-1 Rate=250 Surface=yes [FSIDLE] Image=FSIDLE LoopStart=0 LoopEnd=20 Rate=800 ShouldUseCellDrawer=false Report=FIRSTRM1 [FSAIR] Image=FSAIR LoopStart=0 LoopEnd=20 Rate=800 Report=FIRSTRM1 [FSGRND] Image=FSGRND LoopStart=0 LoopEnd=20 Rate=800 Report=FIRSTRM1 YDrawOffset=-20 [NAWAST] Cameo=WASTICON Image=NAWAST Remapable=yes Foundation=3x3 Buildup=NAWASTMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes BibShape=NAWASTBB ActiveAnim=NAWAST_A ActiveAnimDamaged=NAWAST_AD ActiveAnimZAdjust=-127 ActiveAnimTwo=NAWAST_B ActiveAnimTwoDamaged=NAWAST_BD ActiveAnimTwoZAdjust=-60 [NAOBEL] Cameo=OBLIICON Image=NAOBEL Height=2 Remapable=yes Foundation=2x2 ChargeAnim=yes Buildup=NAOBELMK DemandLoadBuildup=true FreeBuildup=true ;PrimaryFirePixelOffset=10,-51 PrimaryFirePixelOffset=11,-26 NewTheater=yes ActiveAnim=NAOBEL_A ActiveAnimZAdjust=-100 [NAMISL] Cameo=MSSLICON Image=NAMISL Remapable=yes Foundation=2x2 Buildup=NAMISLMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NAMISL_B ActiveAnimDamaged=NAMISL_BD ActiveAnimZAdjust=-100 [GAVULC] Cameo=TWR1ICON Image=GACTWR_B Remapable=yes Foundation=1x1 PrimaryFireFLH=162,30,90 SecondaryFireFLH=162,-30,90 NewTheater=yes [GAROCK] Cameo=TWR2ICON Image=GACTWR_C Remapable=yes Foundation=1x1 PrimaryFireFLH=152,50,192 SecondaryFireFLH=152,-50,192 NewTheater=yes ; SAM addon for component tower [GACSAM] Image=GACTWR_D Remapable=yes Cameo=TWR3ICON Foundation=1x1 MidPoint=66 PrimaryFireFLH=152,50,192 SecondaryFireFLH=152,-50,192 NewTheater=yes ; SAM site turret [NASAM_A] Image=GACTWR_D ;Remapable=yes ;PrimaryFireFLH=90,50,100 ;SecondaryFireFLH=90,-50,100 ;NewTheater=yes [GAFIRE] Cameo=FSDICON Image=GAFIRE Height=2 Remapable=yes Foundation=3x2 Buildup=GAFIREMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=GAFIRE_C ActiveAnimZAdjust=-100 ActiveAnimTwo=GAFIRE_B ActiveAnimTwoZAdjust=-75 [GAFSDF] Image=GAFSDF Remapable=yes Foundation=1x1 NewTheater=yes NormalZAdjust=-10 Cameo=FSPICON [NAPOST] Cameo=LASRICON Image=NAPOST Remapable=yes Foundation=1x1 Buildup=NAPOSTMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes ActiveAnim=NAPOST_A ;ActiveAnimDestroyed=NAPOST_AD ActiveAnimZAdjust=-25 ActiveAnimTwo=NAPOST_B ActiveAnimTwoZAdjust=-100 [NAFNCE] Image=NAFNCE Remapable=yes Foundation=1x1 NewTheater=yes [NALASR] Cameo=PLTICON Image=NALASR Remapable=yes Foundation=1x1 Buildup=NALASRMK DemandLoadBuildup=true FreeBuildup=true NewTheater=yes PrimaryFireFLH=0,0,168 Recoilless=yes PBarrelLength=200 PBarrelThickness=48 [NASAM] Cameo=SAMICON Image=NASAM Remapable=yes Foundation=1x1 Buildup=NASAMMK DemandLoadBuildup=true FreeBuildup=true PrimaryFireFLH=90,50,100 SecondaryFireFLH=90,-50,100 NewTheater=yes [ABAN01] Foundation=2x6 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes Height=3 DemandLoad=true [ABAN02] Foundation=5x3 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes Height=4 ;was 2 DemandLoad=true [ABAN03] Foundation=2x5 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes Height=2 DemandLoad=true [ABAN04] Foundation=4x2 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes DemandLoad=true Height=3 [ABAN05] Foundation=3x2 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes DemandLoad=true Height=2 [ABAN06] Foundation=2x2 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes DemandLoad=true Height=2 [ABAN07] Foundation=2x2 ExtraDamageStage=false TerrainPalette=true ;NewTheater=yes DemandLoad=true Height=2 [ABAN08] Foundation=2x2 ExtraDamageStage=false ;NewTheater=yes TerrainPalette=true DemandLoad=true Height=2 [ABAN09] Foundation=2x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [ABAN10] ;NewTheater=yes Foundation=2x2 ExtraDamageStage=false TerrainPalette=true DemandLoad=true Height=2 [ABAN11] ;NewTheater=yes Foundation=2x2 ExtraDamageStage=false TerrainPalette=true DemandLoad=true Height=2 [ABAN12] ;NewTheater=yes Foundation=2x2 ExtraDamageStage=false TerrainPalette=true DemandLoad=true Height=2 [ABAN13] ;NewTheater=yes Foundation=1x1 ExtraDamageStage=false TerrainPalette=true DemandLoad=true [ABAN14] ;NewTheater=yes Foundation=1x1 ExtraDamageStage=false TerrainPalette=true DemandLoad=true [ABAN15] ;NewTheater=yes Foundation=1x1 ExtraDamageStage=false TerrainPalette=true DemandLoad=true [ABAN16] Foundation=2x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [ABAN17] ;NewTheater=yes Foundation=1x1 ExtraDamageStage=false TerrainPalette=true DemandLoad=true [ABAN18] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true Height=2 [CA0001] Foundation=3x3 Height=1 NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CA0002] Foundation=3x3 Height=1 NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CA0003] Foundation=2x2 NewTheater=yes Height=1 ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CA0004] Foundation=2x2 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0005] Foundation=1x2 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0006] Foundation=1x2 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0007] Foundation=1x2 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0008] Foundation=2x3 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0009] Foundation=2x3 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0010] Foundation=2x2 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0011] Foundation=1x2 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0012] Foundation=1x2 Height=2 ;was 1.5 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0013] Foundation=2x1 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0014] Foundation=1x1 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0015] Foundation=1x1 Height=1 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0016] Foundation=1x1 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0017] Foundation=1x1 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0018] Foundation=1x2 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0019] Foundation=1x2 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0020] Foundation=1x2 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true [CA0021] Foundation=1x2 Height=2 ExtraDamageStage=false TerrainPalette=true NewTheater=yes DemandLoad=true ; Ruined C&C con yard [GAOLDCC1] Foundation=2x2 Height=1 ExtraDamageStage=false NewTheater=yes Remapable=yes DemandLoad=true ; Ruined C&C Temple of NOD [GAOLDCC2] Foundation=2x2 Height=2 ;was 1.5 ExtraDamageStage=false NewTheater=yes Remapable=yes DemandLoad=true ; Ruined C&C Weapons factory. [GAOLDCC3] Foundation=2x2 Height=2 ;was 1.5 ExtraDamageStage=false NewTheater=yes Remapable=yes DemandLoad=true ; Ruined C&C Refinery. [GAOLDCC4] Foundation=2x2 Height=2 ;was 1.5 ExtraDamageStage=false NewTheater=yes Remapable=yes DemandLoad=true ; Ruined C&C Advanced Power. [GAOLDCC5] Foundation=2x2 Height=1 ExtraDamageStage=false NewTheater=yes Remapable=yes DemandLoad=true ; Ruined C&C Silos (2) [GAOLDCC6] Foundation=2x2 Height=1 ExtraDamageStage=false NewTheater=yes Remapable=yes DemandLoad=true [CITY01] Foundation=4x2 Height=3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY02] Foundation=2x3 Height=3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY03] Foundation=3x2 Height=3 ;was 2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY04] Foundation=3x2 Height=4 ;was 3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY05] Foundation=3x2 Height=5 ;was 4 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY06] Foundation=4x2 Height=3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY07] Foundation=4x2 Height=3 ;was 2.5 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY08] Foundation=2x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY09] Foundation=2x2 Height=2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY10] Foundation=2x2 Height=2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY11] Foundation=2x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true Height=4 DemandLoad=true [CITY12] Foundation=2x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true Height=4 DemandLoad=true [CITY13] Foundation=2x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true Height=4 DemandLoad=true [CITY14] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true Height=4 DemandLoad=true [CITY15] Foundation=4x2 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true Height=2 [CITY16] Foundation=4x2 Height=3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY17] Foundation=4x3 Height=5 ;was 4.5 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY18] Foundation=3x5 Height=4 ;was 3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY19] Foundation=2x2 Height=1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY20] Foundation=1x1 Height=1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY21] Foundation=1x1 Height=2 ;was 1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CITY22] Foundation=2x2 Height=2 ;was 3 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [CTVEGA] Foundation=4x4 Height=3 NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD01] Foundation=1x1 ;NewTheater=yes TerrainPalette=true ExtraDamageStage=false DemandLoad=true [BBOARD02] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD03] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD04] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD05] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD06] Foundation=1x2 ;NewTheater=yes TerrainPalette=true ExtraDamageStage=false DemandLoad=true [BBOARD07] Foundation=1x2 ;NewTheater=yes TerrainPalette=true ExtraDamageStage=false DemandLoad=true [BBOARD08] Foundation=1x2 ;NewTheater=yes TerrainPalette=true ExtraDamageStage=false DemandLoad=true [BBOARD09] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD10] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD11] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD12] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD13] Foundation=1x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD14] Foundation=2x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD15] Foundation=2x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BBOARD16] Foundation=2x1 ;NewTheater=yes ExtraDamageStage=false TerrainPalette=true DemandLoad=true [BOXES01] Foundation=1x1 Theater=yes [BOXES02] Foundation=1x1 Theater=yes [BOXES03] Foundation=1x1 Theater=yes [BOXES04] Foundation=1x1 Theater=yes [BOXES05] Foundation=1x1 Theater=yes [BOXES06] Foundation=1x1 Theater=yes [BOXES07] Foundation=1x1 Theater=yes [BOXES08] Foundation=1x1 Theater=yes [BOXES09] Foundation=1x1 Theater=yes [ICE01] Foundation=2x2 Theater=yes [ICE02] Foundation=1x2 Theater=yes [ICE03] Foundation=2x1 Theater=yes [ICE04] Foundation=1x1 Theater=yes [ICE05] Foundation=1x1 Theater=yes [TIBTRE01] Theater=yes Foundation=1x1 [TIBTRE02] Theater=yes Foundation=1x1 [TIBTRE03] Theater=yes Foundation=1x1 [TREE01] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE02] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE03] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE04] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE05] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE06] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE07] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE08] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE09] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE10] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE11] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE12] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE13] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE14] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE15] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE16] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE17] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE18] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE19] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE20] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE21] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE22] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE23] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE24] Theater=yes Foundation=1x1 ;DemandLoad=true [TREE25] Theater=yes Foundation=1x1 ;DemandLoad=true ; Smudges listed here [CRATER01] Theater=yes [CRATER02] Theater=yes [CRATER03] Theater=yes [CRATER04] Theater=yes [CRATER05] Theater=yes [CRATER06] Theater=yes [CRATER07] Theater=yes [CRATER08] Theater=yes [CRATER09] Theater=yes [CRATER10] Theater=yes [CRATER11] Theater=yes [CRATER12] Theater=yes [BURNT01] Theater=yes [BURNT02] Theater=yes [BURNT03] Theater=yes [BURNT04] Theater=yes [BURNT05] Theater=yes [BURNT06] Theater=yes [BURNT07] Theater=yes [BURNT08] Theater=yes [BURNT09] Theater=yes [BURNT10] Theater=yes [BURNT11] Theater=yes [BURNT12] Theater=yes ;[CR1] ;Theater=yes ;[CR2] ;Theater=yes ;[CR3] ;Theater=yes ;[CR4] ;Theater=yes ;[CR5] ;Theater=yes ;[CR6] ;Theater=yes ;[BURN01] ;Theater=yes ;[BURN02] ;Theater=yes ;[BURN03] ;Theater=yes ;[BURN04] ;Theater=yes ;[BURN05] ;Theater=yes ;[BURN06] ;Theater=yes ;[BURN07] ;Theater=yes ;[BURN08] ;Theater=yes ;[BURN09] ;Theater=yes ;[BURN10] ;Theater=yes ;[BURN11] ;Theater=yes ;[BURN12] ;Theater=yes ;[BURN13] ;Theater=yes ;[BURN14] ;Theater=yes ;[BURN15] ;Theater=yes ;[BURN16] ;Theater=yes [BRIDGE] Theater=yes [CRATE] Theater=yes [RAILBRDG] Theater=yes [TIB01] Theater=yes [TIB02] Theater=yes [TIB03] Theater=yes [TIB04] Theater=yes [TIB05] Theater=yes [TIB06] Theater=yes [TIB07] Theater=yes [TIB08] Theater=yes [TIB09] Theater=yes [TIB10] Theater=yes [TIB11] Theater=yes [TIB12] Theater=yes [TIB13] Theater=yes [TIB14] Theater=yes [TIB15] Theater=yes [TIB16] Theater=yes [TIB17] Theater=yes [TIB18] Theater=yes [TIB19] Theater=yes [TIB20] Theater=yes ;[BTIB01] ;Theater=yes ;[BTIB02] ;Theater=yes ;[BTIB03] ;Theater=yes [TRACKS01] Theater=yes DemandLoad=true [TRACKS02] Theater=yes DemandLoad=true [TRACKS03] Theater=yes DemandLoad=true [TRACKS04] Theater=yes DemandLoad=true [TRACKS05] Theater=yes DemandLoad=true [TRACKS06] Theater=yes DemandLoad=true [TRACKS07] Theater=yes DemandLoad=true [TRACKS08] Theater=yes DemandLoad=true [TRACKS09] Theater=yes DemandLoad=true [TRACKS10] Theater=yes DemandLoad=true [TRACKS11] Theater=yes DemandLoad=true [TRACKS12] Theater=yes DemandLoad=true [TRACKS13] Theater=yes DemandLoad=true [TRACKS14] Theater=yes DemandLoad=true [TRACKS15] Theater=yes DemandLoad=true [TRACKS16] Theater=yes DemandLoad=true [SROCK01] Theater=yes [SROCK02] Theater=yes [SROCK03] Theater=yes [SROCK04] Theater=yes [SROCK05] Theater=yes [TROCK01] Theater=yes [TROCK02] Theater=yes [TROCK03] Theater=yes [TROCK04] Theater=yes [TROCK05] Theater=yes [TRTUNN01] ;TerrainPalette=yes Theater=yes ;Foundation=0x0 DemandLoad=true [TRTUNN02] ;TerrainPalette=yes Theater=yes ;Foundation=0x0 DemandLoad=true [TRTUNN03] ;TerrainPalette=yes Theater=yes ;Foundation=0x0 DemandLoad=true [TRTUNN04] ;TerrainPalette=yes Theater=yes ;Foundation=0x0 DemandLoad=true [VEINS] Theater=yes DemandLoad=true [VEINHOLE] Theater=yes [VEINATAC] Theater=yes Rate=300 LoopStart=0 LoopEnd=12 LoopCount=-1 IsVeins=true DemandLoad=true [LOBRDG01] Theater=yes DemandLoad=true [LOBRDG02] Theater=yes DemandLoad=true [LOBRDG03] Theater=yes DemandLoad=true [LOBRDG04] Theater=yes DemandLoad=true [LOBRDG05] Theater=yes DemandLoad=true [LOBRDG06] Theater=yes DemandLoad=true [LOBRDG07] Theater=yes DemandLoad=true [LOBRDG08] Theater=yes DemandLoad=true [LOBRDG09] Theater=yes DemandLoad=true [LOBRDG10] Theater=yes DemandLoad=true [LOBRDG11] Theater=yes DemandLoad=true [LOBRDG12] Theater=yes DemandLoad=true [LOBRDG13] Theater=yes DemandLoad=true [LOBRDG14] Theater=yes DemandLoad=true [LOBRDG15] Theater=yes DemandLoad=true [LOBRDG16] Theater=yes DemandLoad=true [LOBRDG17] Theater=yes DemandLoad=true [LOBRDG18] Theater=yes DemandLoad=true [LOBRDG19] Theater=yes DemandLoad=true [LOBRDG20] Theater=yes DemandLoad=true [LOBRDG21] Theater=yes DemandLoad=true [LOBRDG22] Theater=yes DemandLoad=true [LOBRDG23] Theater=yes DemandLoad=true [LOBRDG24] Theater=yes DemandLoad=true [LOBRDG25] Theater=yes DemandLoad=true [LOBRDG26] Theater=yes DemandLoad=true [LOBRDG27] Theater=yes DemandLoad=true [LOBRDG28] Theater=yes DemandLoad=true [LOBRDGE1] Theater=yes DemandLoad=true [LOBRDGE2] Theater=yes DemandLoad=true [LOBRDGE3] Theater=yes DemandLoad=true [LOBRDGE4] Theater=yes DemandLoad=true [FPLS] ;[WCRATE] ;[WWCRATE] ;[SCRATE] ; *** Infantry Sequences *** ; Infantry animations are grouped within a single art file. ; Unlike units, infantry animation layout is completely ; arbitrary and must be explicitly specified. Each ; infantry format file will be identified with one of these ; animation sequences. ; ; The first number is the starting frame number. The second ; number is the number of frames of the animation. If this ; number is zero then the anim sequence is not present. ; The third number is the multiplier by the infantry facing ; to reach the facing specific animation start. If this ; last number is zero, then there is no facing specific ; version. ; ; Ready = standing around ; Guard = standing around with weapon drawn ; Prone = while prone ; Walk = walking [normal movement] ; FireUp = firing while standing ; Down = transition from standing to prone ; Crawl = moving while prone ; Up = transition from prone to standing ; FireProne = firing while prone ; Idle1 = idle animation sequence #1 ; Idle2 = idle animation sequence #2 ; Die1 = death animation when hit by gunfire ; Die2 = death animation when exploding ; Die3 = death animation when exploding (alternate) ; Die4 = death animation by concussion explosion ; Die5 = death animatino by fire ; Fly = [jumpjet] flying ; Hover = [jumpjet] hovering ; Tumble = [jumpjet] tumbling ; FireFly = [jumpjet] firing while flying [DoggieSequence] Ready=0,1,1 Guard=0,1,1 Prone=90,1,0 Walk=8,6,6 FireUp=56,4,4 Down=88,3,0 Crawl=0,0,0 Up=89,1,0 FireProne=0,0,0 Idle1=91,8,0,E Idle2=91,8,0,E Die1=99,10,0 Die2=99,10,0 Die3=99,10,0 Die4=99,10,0 Die5=109,10,0 Struggle=0,6,0 ;firestorm addition [E1Sequence] Ready=0,1,1 Guard=0,1,1 Prone=86,1,6 Walk=8,6,6 FireUp=164,6,6 Down=260,2,2 Crawl=86,6,6 Up=276,2,2 FireProne=212,6,6 Idle1=56,15,0,W Idle2=71,14,0,E Die1=134,15,0 Die2=149,15,0 Die3=0,1,1 Die4=0,1,1 Die5=0,1,1 Struggle=0,6,0 ;firestorm addition [MedicSequence] Ready=0,1,1 Guard=0,1,1 Prone=86,1,6 Walk=8,6,6 FireUp=292,15,0 Down=260,2,2 Crawl=86,6,6 Up=276,2,2 FireProne=292,15,0 Idle1=56,15,0,W Idle2=71,14,0,E Die1=134,15,0 Die2=149,15,0 Die3=0,1,1 Die4=0,1,1 Die5=0,1,1 Struggle=0,6,0 ;firestorm addition [JumpjetSequence] Ready=0,1,1 Guard=0,1,1 Prone=0,1,1 Walk=8,6,6 FireUp=164,6,6 Down=0,1,1 Crawl=8,6,6 Up=0,1,1 FireProne=164,6,6 Idle1=56,15,0,W Idle2=71,15,0,E Die1=134,15,0 Die2=149,15,0 Die3=0,0,0 Die4=0,0,0 Die5=0,0,0 Fly=292,6,6 Hover=340,6,6 FireFly=388,6,6 Tumble=436,15,0 Struggle=0,6,0 ;firestorm addition [CyborgSequence] Ready=0,1,1 Guard=0,1,1 Prone=110,1,9 Walk=8,9,9 FireUp=212,6,6 Down=0,1,1 Crawl=110,9,9 Up=0,1,1 FireProne=260,6,6 Idle1=80,15,0,W Idle2=95,15,0,E Die1=182,15,0 Die2=197,15,0 Die3=0,0,0 Die4=0,0,0 Die5=0,0,0 Struggle=0,6,0 ;firestorm addition ; *** Projectile Objects *** ; Projectiles sometimes need additional information regarding their ; imagery. ; Trailer = animation to spawn as the projectile moves [typically smoke] (def=none) ; Rotates = Does projectile have specific imagery according to facing (def=no)? ; Frames = number of image frames for animation purposes (def=1) ; AnimPalette = Does it use the animation palette palette (def=no)? [120MM] [DRAGON] Trailer=SMOKEY2 Rotates=yes [MISLCHEM] Trailer=SMOKEY2 Voxel=yes [MISLMLTI] Trailer=SMOKEY2 Voxel=yes [TORPEDO] AnimPalette=yes AnimLow=0 AnimHigh=2 AnimRate=1 [DISCUS] AnimPalette=yes AnimLow=0 AnimHigh=5 AnimRate=1 [CANISTER] AnimLow=0 AnimHigh=13 AnimRate=1 [BOMB] AnimPalette=yes AnimLow=0 AnimHigh=13 AnimRate=1 [MISSILE] Trailer=SMOKEY2 Rotates=yes [PATRIOT] Trailer=SMOKEY Rotates=yes ; *** Animation Overlays *** ; These are the temporary animations overlays that are used for such ; effects as explosions, smoke, and fire. ; Theater = Is there theater specific art for this animation (def=no)? ; Normalized = Should the animation speed be adjusted to appear consistent (def=no)? ; Scorch = Does this animation scorch the ground [e.g., napalm does this] (def=no)? ; Flamer = Does this animation leave flames after it is gone [e.g., napalm] (def=no)? ; Crater = Does this form a crater [e.g., artillery does this] (def=no)? ; Sticky = Animation sticks to unit in square (def=no)? ; Surface = Is this animation at ground level (def=no)? ; Flat = Is animation flat on the ground [helps with drawing logic to know this] (def=no)? ; Translucent = Is this animation translucent in appearence (def=no)? ; Translucency = percent of translucency to use [25, 50, 75% only] (def=none) ; Damage = damage to apply per minute to attached object [if any] (def=0) ; Rate = desired animation frames per minute (def=900) ; Report = sound effect to play when this animation plays (def=none) ; Next = animation to spawn when this animation completes [fire uses this to get smaller over time] (def=none> ; DetailLevel = The detail level that the game must be set to for this animation to play (def=0, 2 is high detail). ; Start = Frame to start this animation from. ; UseNormalLight = Does this anim always draw at 100% brightness? (def=no) ; YSortAdjust = Fudge to apply when sorting this object in it's layer (def=0). ; AltPalette = Does it use an alternate drawing palette (def=no)? ; <<< these values are needed if the animation loops >>> ; LoopCount = number of times this animation loops before ending (def=0)? ; LoopStart = beginning frame of loop [if animation loops] ; LoopEnd = last frame of loop [if animation loops] (def=last frame of animation) ; <<< values used only for wave-based animations ; Angle = angle in degrees to use for the cone of effect (between 0 and 180 please) [BEACON] Surface=yes LoopCount=100 Rate=60 [WAKE1] ;Theater=yes Flat=yes Surface=yes Translucent=yes Rate=120 YSortAdjust=-64 [WAKE2] ;Theater=yes Flat=yes Surface=yes Translucent=yes Rate=120 YSortAdjust=-64 [STEAMPUF] Flat=yes Surface=yes Translucent=yes [DROPPOD] Rate=10 Flat=yes DetailLevel=1 Surface=yes Translucent=yes [DROPPOD2] Rate=10 Flat=yes DetailLevel=1 Surface=yes Translucent=yes [DEATH_A] Rate=10 Flat=yes DetailLevel=1 Surface=yes [DEATH_B] Rate=10 Flat=yes DetailLevel=1 Surface=yes [DEATH_C] Rate=10 Flat=yes DetailLevel=1 Surface=yes [DEATH_D] Rate=10 Flat=yes DetailLevel=1 Surface=yes [DEATH_E] Rate=10 Flat=yes DetailLevel=1 Surface=yes [DEATH_F] Rate=10 Flat=yes DetailLevel=1 Surface=yes [DBRIS1LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes TrailerAnim=SMOKEY2 TrailerSeperation=2 [DBRIS1SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS2LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS2SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS3LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS3SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS4LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS4SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS5LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes TrailerAnim=SMOKEY2 TrailerSeperation=2 [DBRIS5SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS6LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS6SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS7LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS7SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS8LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes TrailerAnim=SMOKEY2 TrailerSeperation=2 [DBRIS8SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS9LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRIS9SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRS10LG] Elasticity=0.0 MaxXYVel=25.0 MinZVel=25.0 ExpireAnim=TWLT036 Damage=20 DamageRadius=80 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [DBRS10SM] Elasticity=0.0 MaxXYVel=30.0 MinZVel=20.0 ExpireAnim=TWLT026 Damage=10 DamageRadius=50 Warhead=HE LoopStart=0 LoopEnd=15 LoopCount=-1 RandomRate=220,600 DetailLevel=0 Bouncer=yes [TREESPRD] Normalized=yes ; Tesla Coil zap animation (infantry) [ELECTRO] Scorch=yes Surface=yes LoopEnd=3 LoopCount=5 Next=FIRE1 Report=ELECTRO1 ; SAM site fire animation [SAM-N] [SAM-NE] [SAM-NW] [SAM-E] [SAM-W] [SAM-SW] [SAM-SE] [SAM-S] [DIG] Surface=yes [INFDIE] Normalized=yes Surface=yes [DIRTEXPL] Normalized=yes Surface=yes Translucent=yes Report=EXPDIRT1 ;[PULSGRNL] ;Normalized=yes ;Surface=yes ;Translucent=yes ;[PULSGRNS] ;Normalized=yes ;Surface=yes ;Translucent=yes ;[PULSREDL] ;Normalized=yes ;Surface=yes ;Translucent=yes ;[PULSREDS] ;Normalized=yes ;Surface=yes ;Translucent=yes ;[PULSWHTL] ;Normalized=yes ;Surface=yes ;Translucent=yes ;[PULSWHTS] ;Normalized=yes ;Surface=yes ;Translucent=yes [PULSEFX1] Normalized=yes Surface=yes Translucent=yes UseNormalLight=yes ;AltPalette=yes ;Translucency=50 [PULSEFX2] Normalized=yes Surface=yes Translucent=yes UseNormalLight=yes ;AltPalette=yes ;Translucency=50 [EMP_FX01] Normalized=yes Surface=yes UseNormalLight=yes LoopCount=-1 ; smoke used as landing zone marker [SMOKLAND] Normalized=yes Surface=yes LoopStart=72 LoopEnd=91 LoopCount=255 [SGRYSMK1] Rate=500 Normalized=yes Translucent=yes ; small sticky fire [BURN-S] Surface=yes Sticky=yes Damage=.03 Rate=400 LoopStart=30 LoopEnd=62 LoopCount=4 UseNormalLight=yes ;Next=SMOKE_M Translucency=25 ; medium sticky fire [BURN-M] Surface=yes Sticky=yes Damage=.06 Rate=400 LoopStart=30 LoopEnd=62 LoopCount=4 Next=BURN-S UseNormalLight=yes Translucency=25 ; large sticky fire [BURN-L] Surface=yes Sticky=yes Damage=.06 Rate=400 LoopStart=30 LoopEnd=62 LoopCount=4 Next=BURN-M UseNormalLight=yes Translucency=25 ; parachute to attach to parachutists [PARACH] Rate=200 LoopStart=7 LoopCount=15 ; parachute bomb [PARABOMB] Rate=200 LoopStart=7 LoopCount=15 AnimLow=8 AnimHigh=12 AnimRate=1 [TWLT026] Normalized=yes Translucent=yes Report=EXPNEW05 UseNormalLight=yes Crater=yes Scorch=yes [TWLT036] Normalized=yes Translucent=yes Report=EXPNEW06 UseNormalLight=yes Crater=yes Scorch=yes [TWLT050] Normalized=yes Translucent=yes Report=EXPNEW07 UseNormalLight=yes Crater=yes Scorch=yes [TWLT070] Normalized=yes Translucent=yes Report=EXPNEW09 UseNormalLight=yes Crater=yes Scorch=yes [TWLT070T] Image=TWLT070 Normalized=yes Translucent=yes TiberiumChainReaction=yes Report=EXPNEW05 UseNormalLight=yes Crater=yes Scorch=yes [TWLT100] Normalized=yes Translucent=yes Report=EXPNEW10 UseNormalLight=yes Crater=yes Scorch=yes [TWLT100I] Image=TWLT100 Normalized=yes Translucency=50 Report=EXPNEW11 UseNormalLight=yes [RING1] Translucent=yes Rate=300 TranslucencyDetailLevel=1 Flat=true [IONBEAM] Translucent=yes Rate=200 Tiled=yes TranslucencyDetailLevel=1 Report=ION1 [S_BANG16] Normalized=yes Translucent=yes Report=EXPNEW10 UseNormalLight=yes Crater=yes Scorch=yes [S_BANG24] Normalized=yes Translucent=yes Report=EXPNEW10 UseNormalLight=yes Crater=yes Scorch=yes [S_BANG34] Normalized=yes Translucent=yes Report=EXPNEW10 UseNormalLight=yes Crater=yes Scorch=yes [S_BANG48] Normalized=yes Translucent=yes Report=EXPNEW11 UseNormalLight=yes Crater=yes Scorch=yes [S_BRNL20] Normalized=yes Translucent=yes Report=EXPNEW12 UseNormalLight=yes Crater=yes Scorch=yes [S_BRNL30] Normalized=yes Translucent=yes Report=EXPNEW12 UseNormalLight=yes Crater=yes Scorch=yes [S_BRNL40] Normalized=yes Translucent=yes Report=EXPNEW12 UseNormalLight=yes Crater=yes Scorch=yes [S_BRNL58] Normalized=yes Translucent=yes Report=EXPNEW12 UseNormalLight=yes Crater=yes Scorch=yes [S_CLSN16] Normalized=yes Translucent=yes Report=EXPNEW14 UseNormalLight=yes Crater=yes [S_CLSN22] Normalized=yes Translucent=yes Report=EXPNEW14 UseNormalLight=yes Crater=yes [S_CLSN30] Normalized=yes Translucent=yes Report=EXPNEW14 UseNormalLight=yes Crater=yes [S_CLSN42] Normalized=yes Translucent=yes Report=EXPNEW14 UseNormalLight=yes Crater=yes [S_CLSN58] Normalized=yes Translucent=yes Report=EXPNEW14 UseNormalLight=yes Crater=yes [S_TUMU22] Normalized=yes Translucent=yes Report=EXPNEW15 UseNormalLight=yes Crater=yes Scorch=yes [S_TUMU30] Normalized=yes Translucent=yes Report=EXPNEW15 UseNormalLight=yes Crater=yes Scorch=yes [S_TUMU42] Normalized=yes Translucent=yes Report=EXPNEW15 UseNormalLight=yes Crater=yes Scorch=yes [S_TUMU60] Normalized=yes Translucent=yes Report=EXPNEW15 UseNormalLight=yes Crater=yes Scorch=yes ; smoke puff used by rockets [SMOKEY] Translucent=yes [SMOKEY2] Translucent=yes ; small arms fire piff (single shot) [PIFF] ;Normalized=yes ;Theater=yes ;Translucent=yes ; small arms fire piff (multiple shots) [PIFFPIFF] ;Theater=yes ;Normalized=yes ;Translucent=yes ; small flames [FIRE3] Surface=yes Damage=.003 LoopCount=5 Rate=450 UseNormalLight=yes Translucency=50 ; medium flames [FIRE1] Scorch=yes Surface=yes Damage=.006 Rate=450 LoopCount=4 UseNormalLight=yes Translucency=50 Next=FIRE2 ; medium flames [FIRE2] Scorch=yes Surface=yes Damage=.006 Rate=450 LoopCount=6 UseNormalLight=yes Translucency=50 Next=FIRE3 ; tiny flames [FIRE4] Surface=yes Damage=.002 LoopCount=3 UseNormalLight=yes Translucency=25 ; muzzle flash [GUNFIRE] Surface=yes Translucent=yes [XGRYSML1] Translucent=yes Report=EXPNEW13 [XGRYSML2] Translucent=yes Report=EXPNEW13 [XGRYMED1] Translucent=yes Report=EXPNEW15 [XGRYMED2] Translucent=yes Report=EXPNEW12 [EXPLOLRG] Translucent=yes UseNormalLight=yes Report=EXPNEW09 Crater=yes Scorch=yes [PODRING] Translucent=yes Flat=yes [CLDRNGL1] Surface=yes Flat=yes [CLDRNGL2] Surface=yes Flat=yes [CLDRNGMD] Surface=yes Flat=yes [CLDRNGSM] Surface=yes Flat=yes [DROPEXP] Surface=yes Translucent=yes [INVISO] Surface=yes Damage=1 [EXPLOMED] Translucent=yes UseNormalLight=yes Report=EXPNEW12 Crater=yes Scorch=yes [EXPLOSML] Translucent=yes UseNormalLight=yes Report=EXPNEW13 Crater=yes Scorch=yes ; minigun fire flashes [MGUN-N] [MGUN-NE] [MGUN-E] [MGUN-SE] [MGUN-S] [MGUN-SW] [MGUN-W] [MGUN-NW] ; Armor bonus [ARMOR] Normalized=yes Rate=400 ; Money bonus [MONEY] Normalized=yes Rate=400 ; Firepower bonus crate animation [FIREPOWR] Normalized=yes Rate=400 [VETERAN] Normalized=yes Rate=400 [REVEAL] Normalized=yes Rate=400 [SHROUDX] Normalized=yes Rate=400 [MLTIMISL] Normalized=yes Rate=400 [HEALONE] Normalized=yes Rate=400 [HEALALL] Normalized=yes Rate=400 [CHEMISLE] Normalized=yes Rate=400 [CLOAK] Normalized=yes Rate=400 ; twinkle animation [TWINKLE1] Normalized=yes LoopCount=5 [TWINKLE2] Normalized=yes LoopCount=7 [TWINKLE3] Normalized=yes LoopCount=3 ; large water explosion [H2O_EXP1] Normalized=yes Surface=yes AltPalette=yes Translucency=50 Report=SSPLASH1 YDrawOffset=-18 ; medium water explosion [H2O_EXP2] Normalized=yes Surface=yes AltPalette=yes Translucency=50 Report=SSPLASH2 YDrawOffset=-10 ; small water explosion [H2O_EXP3] Normalized=yes Surface=yes AltPalette=yes Translucency=50 Report=SSPLASH3 YDrawOffset=-8 ; expanding fire ring [RING] Normalized=yes Translucent=yes ; Power plant active animation [GAPOWR_A] Image=GAPOWR_A Normalized=yes NewTheater=yes Surface=yes Start=0 LoopStart=0 LoopEnd=12 LoopCount=-1 Rate=220 DetailLevel=1 ; Power plant active animation [GAPOWR_AD] Image=GAPOWR_A Normalized=yes ;NewTheater=yes Surface=yes Start=12 LoopStart=12 LoopEnd=24 LoopCount=-1 Rate=220 DetailLevel=1 ; Power plant power-up animations [GAPOWR_B] Cameo=TURBICON Image=GAPOWR_B NewTheater=yes Normalized=yes Surface=yes LoopEnd=12 LoopCount=-1 Rate=220 ; NOD Power plant active animation [NAPOWR_A] Image=NAPOWR_A Normalized=yes NewTheater=yes Surface=yes Start=0 LoopStart=0 LoopEnd=9 LoopCount=-1 Rate=200 DetailLevel=1 ; NOD Power plant damaged active animation [NAPOWR_AD] Image=NAPOWR_A Normalized=yes ;NewTheater=yes Surface=yes Start=9 LoopStart=9 LoopEnd=18 LoopCount=-1 Rate=220 DetailLevel=1 ; NOD Advanced power plant active animation [NAAPWR_A] Image=NAAPWR_A Normalized=yes NewTheater=yes Surface=yes Start=0 LoopStart=0 LoopEnd=9 LoopCount=-1 Rate=220 DetailLevel=1 ; NOD Advanced power plant damaged active animation [NAAPWR_AD] Image=NAAPWR_A Normalized=yes ;NewTheater=yes Surface=yes Start=9 LoopStart=9 LoopEnd=18 LoopCount=-1 Rate=220 DetailLevel=1 ; Civilian Hospital [CAHOSP_A] Image=CAHOSP_A Normalized=yes NewTheater=yes Surface=yes LoopStart=0 LoopEnd=9 LoopCount=-1 Rate=350 DetailLevel=2 DemandLoad=true ; Civilian Armory [CAARMR_A] Image=CAARMR_A Normalized=yes NewTheater=yes Surface=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 DemandLoad=true DetailLevel=2 [NARADR_A] Image=NARADR_A NewTheater=yes Normalized=yes LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=220 Surface=yes [NARADR_AD] Image=NARADR_A ;NewTheater=yes Normalized=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=180 Surface=yes [GARADR_A] NewTheater=yes Normalized=yes LoopStart=0 LoopEnd=28 LoopCount=-1 Rate=220 ;Surface=yes PingPong=no [GARADR_AD] NewTheater=yes Normalized=yes LoopStart=0 LoopEnd=28 LoopCount=-1 Rate=180 ;Surface=yes PingPong=no [GAPLUG_A] Image=GAPLUG_A NewTheater=yes Normalized=yes LoopStart=0 LoopEnd=20 LoopCount=-1 Rate=220 Surface=yes [GAPLUG_B] Image=GAPLUG_B NewTheater=yes Normalized=yes Start=0 LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=220 Surface=yes PingPong=no [GAPLUG_BD] Image=GAPLUG_B ;NewTheater=yes Normalized=yes Start=10 LoopStart=10 LoopEnd=20 LoopCount=-1 Rate=180 Surface=yes PingPong=no [GAPLUG_C] Image=GAPLUG_C NewTheater=yes Normalized=yes Start=0 LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Surface=yes PingPong=no [GAPLUG_D] Image=GAPLUG_D NewTheater=yes Cameo=RAD1ICON Normalized=yes LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=220 ;Surface=yes [GAPLUG_E] Image=GAPLUG_E NewTheater=yes Cameo=RAD2ICON Normalized=yes LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=220 ;Surface=yes [GAPLUG_F] Image=GAPLUG_F NewTheater=yes Cameo=RAD3ICON Normalized=yes LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=220 ;Surface=yes PingPong=yes [GAPILE_A] Image=GAPILE_A NewTheater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=300 Surface=yes DetailLevel=1 [GAPILE_B] Image=GAPILE_B NewTheater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=300 Surface=yes DetailLevel=1 [GAPILE_C] Image=GAPILE_C Normalized=yes NewTheater=yes LoopStart=0 LoopEnd=7 LoopCount=-1 Rate=220 Surface=yes [GAPILE_CD] Image=GAPILE_C Normalized=yes ;NewTheater=yes Start=7 LoopStart=7 LoopEnd=14 LoopCount=-1 Rate=180 Surface=yes [GAWEAP_A] Normalized=yes NewTheater=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=400 Surface=yes DetailLevel=1 [GAWEAP_B] Normalized=yes NewTheater=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=400 Surface=yes DetailLevel=1 [GAWEAP_C] Normalized=yes NewTheater=yes LoopStart=0 LoopEnd=4 LoopCount=-1 Rate=800 Surface=yes DetailLevel=1 [NAWEAP_A] Image=NAWEAP_A Normalized=yes NewTheater=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=400 Surface=yes DetailLevel=1 [NAWEAP_AD] Image=NAWEAP_A Normalized=yes NewTheater=yes Start=16 LoopStart=16 LoopEnd=32 LoopCount=-1 Rate=400 Surface=yes DetailLevel=1 [NAPULS_A] LoopStart=0 NewTheater=yes LoopEnd=64 LoopCount=-1 Rate=0 Surface=yes [GACTWR_A] LoopStart=0 LoopEnd=6 NewTheater=yes LoopCount=-1 Rate=220 Surface=yes Normalized=yes DetailLevel=1 ; Component tower double gun turret [GACTWR_B] LoopStart=0 LoopEnd=32 LoopCount=-1 Rate=0 Surface=yes NewTheater=yes ; Component tower rocket launcher [GACTWR_C] LoopStart=0 LoopEnd=32 LoopCount=-1 Rate=0 Surface=yes NewTheater=yes ; Component tower SAM [GACTWR_D] LoopStart=0 LoopEnd=32 LoopCount=-1 Rate=0 Surface=yes NewTheater=yes ; Active animation for stealth generator [NASTLH_A] Normalized=yes LoopStart=0 LoopEnd=6 LoopCount=-1 Rate=350 Surface=yes PingPong=no NewTheater=yes DetailLevel=1 ; Damaged active animation for stealth generator [NASTLH_AD] Normalized=yes LoopStart=0 LoopEnd=6 LoopCount=-1 Rate=220 Surface=yes PingPong=no NewTheater=yes DetailLevel=1 ; Active animation for construction yard [GACNST_A] Image=GACNST_A Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Damaged active animation for construction yard [GACNST_AD] Image=GACNST_A Normalized=yes Start=10 LoopStart=10 LoopEnd=20 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 ; Third active animation for construction yard [GACNST_B] Image=GACNST_B Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Second active animation for construction yard. [GACNST_C] Image=GACNST_C Normalized=yes LoopStart=0 LoopEnd=15 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Damaged second active animation for construction yard. [GACNST_CD] Image=GACNST_C Normalized=yes Start=15 LoopStart=15 LoopEnd=30 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 ; Production anim for construction yard [GACNST_D] Image=GACNST_D Normalized=yes LoopStart=0 LoopEnd=20 LoopCount=1 Rate=350 Surface=yes NewTheater=yes ;Report=FACBLD1 ; Active animation for hand of nod [NAHAND_A] Image=NAHAND_A Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ActiveAnimTwoZAdjust=-100 ; Second active animation for hand of nod [NAHAND_B] Image=NAHAND_B Normalized=yes LoopStart=0 LoopEnd=12 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Second active animation for hand of nod - damaged version [NAHAND_BD] Image=NAHAND_B Normalized=yes Start=12 LoopStart=12 LoopEnd=24 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 ; Active animation for Temple of NOD [NATMPL_A] Image=NATMPL_A Normalized=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes ; Active animation for NOD Pyramid [NTPYRA_A] Image=NTPYRA_A Normalized=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes ; Damaged active animation for NOD Pyramid [NTPYRA_AD] Image=NTPYRA_A Normalized=yes Start=16 LoopStart=16 LoopEnd=32 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes ; Active animation for NOD Montauk [NAMNTK_A] Image=NAMNTK_A Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DemandLoad=true ; Active animation for GDI Kodiak [GAKODK_A] Image=GAKODK_A Normalized=yes LoopStart=0 LoopEnd=12 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DemandLoad=true ; Damaged active animation for GDI Kodiak [GAKODK_AD] Image=GAKODK_A Normalized=yes Start=12 LoopStart=12 LoopEnd=24 LoopCount=-1 Rate=220 Surface=yes NewTheater=yes DemandLoad=true ; Second active animation for GDI Kodiak [GAKODK_B] Image=GAKODK_B Normalized=yes LoopStart=0 LoopEnd=22 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DemandLoad=true ; Third active animation for GDI Kodiak [GAKODK_C] Image=GAKODK_C Normalized=yes LoopStart=0 LoopEnd=15 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DemandLoad=true ; Third damaged active animation for GDI Kodiak. This is a placeholder and just stops the non-damaged anim from playing [GAKODK_CD] Image=GAKODK_C Normalized=yes LoopStart=0 LoopEnd=0 LoopCount=0 Rate=220 Surface=yes DemandLoad=true ; Animation of tiberium leaving harvester and entering refinery [NAREFN_A] Image=NAREFN_A Normalized=yes LoopStart=0 LoopEnd=5 LoopCount=1 Rate=200 Surface=yes NewTheater=yes ; NAREFN_A but backwards [NAREFN_AR] Image=NAREFN_A Normalized=yes LoopStart=0 LoopEnd=5 LoopCount=1 Reverse=yes Rate=200 Surface=yes ;NewTheater=yes ; Active animation for refinery [NAREFN_C] Image=NAREFN_C Normalized=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes ; Active animation for refinery. Fire ball. [NAREFN_B] Image=NAREFN_B Normalized=yes LoopStart=0 LoopEnd=20 LoopCount=-1 Rate=350 Surface=yes RandomLoopDelay=10,300 NewTheater=yes DetailLevel=1 ShouldUseCellDrawer=false ;Translucency=50 Translucent=yes UseNormalLight=yes ; Active animation for helipad. [GAHPAD_A] Image=GAHPAD_A Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Damaged active animation for helipad. [GAHPAD_AD] Image=GAHPAD_A Normalized=yes LoopStart=8 LoopEnd=16 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 ; Active animation for Nod helipad. [NAHPAD_A] Image=NAHPAD_A Normalized=yes LoopStart=0 LoopEnd=46 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Damaged active animation for Nod helipad. [NAHPAD_AD] Image=NAHPAD_A Normalized=yes Start=46 LoopStart=46 LoopEnd=92 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 ; Repair bay active animation [GADEPT_A] Image=GADEPT_A Normalized=yes LoopStart=0 LoopEnd=5 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ; Damaged repair bay active animation [GADEPT_AD] Image=GADEPT_A Normalized=yes Start=5 LoopStart=5 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes ;NewTheater=yes DetailLevel=1 ; Repair bay active animation [GADEPT_B] Image=GADEPT_B Normalized=yes LoopStart=0 LoopEnd=7 LoopCount=-1 Rate=220 Surface=yes NewTheater=yes DetailLevel=1 ; Repair bay working animation [GADEPT_D] Image=GADEPT_D Normalized=yes LoopStart=0 LoopEnd=6 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes ; Damaged repair bay working animation [GADEPT_DD] Image=GADEPT_D Normalized=yes Start=7 LoopStart=7 LoopEnd=14 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes ; Repair bay arm extending. [GADEPT_C1] Image=GADEPT_C Normalized=yes Start=0 End=5 Rate=400 NewTheater=yes Surface=no ; Repair bay arm working [GADEPT_C2] Image=GADEPT_C Normalized=yes Start=5 LoopStart=5 LoopEnd=11 LoopCount=-1 Rate=400 Surface=no NewTheater=yes ; Repair bay arm retracting [GADEPT_C3] Image=GADEPT_C Normalized=yes Start=11 End=16 Rate=400 Surface=no NewTheater=yes [GATECH_A] Image=GATECH_A Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Surface=yes NewTheater=yes DetailLevel=1 [GATECH_AD] Image=GATECH_A Normalized=yes Start=8 LoopStart=8 LoopEnd=16 LoopCount=-1 Rate=190 Surface=yes ;NewTheater=yes DetailLevel=1 [NATECH_A] Image=NATECH_A Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 [NATECH_AD] Image=NATECH_A Normalized=yes Start=10 LoopStart=10 LoopEnd=20 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 ;[GADROP_A] ;Image=GADROP_A ;Normalized=yes ;LoopStart=0 ;LoopEnd=20 ;LoopCount=-1 ;Rate=350 ;Surface=yes ;NewTheater=yes ;[GADROP_B] ;Image=GADROP_B ;Normalized=yes ;LoopStart=0 ;LoopEnd=20 ;LoopCount=-1 ;Rate=350 ;Surface=yes ;NewTheater=yes ;[GADROP_BD] ;Image=GADROP_B ;Normalized=yes ;Start=20 ;LoopStart=20 ;LoopEnd=40 ;LoopCount=-1 ;Rate=220 ;Surface=yes ;NewTheater=yes [NAWAST_A] Image=NAWAST_A Normalized=yes LoopStart=0 LoopEnd=20 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 [NAWAST_AD] Image=NAWAST_A Normalized=yes Start=20 LoopStart=20 LoopEnd=40 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 [NAWAST_B] Image=NAWAST_B Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 [NAWAST_BD] Image=NAWAST_B Normalized=yes Start=8 LoopStart=8 LoopEnd=16 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 [NAOBEL_A] Image=NAOBEL_A Normalized=yes LoopStart=0 LoopEnd=12 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes ; Obelisk charging animation. [NAOBEL_B] Image=NAOBEL_B Normalized=yes Start=0 LoopStart=0 LoopEnd=12 Rate=0 Surface=yes NewTheater=yes ; Missile silo launch anim [NAMISL_A] Image=NAMISL_A Normalized=yes LoopStart=0 LoopEnd=11 LoopCount=1 Rate=350 Surface=yes NewTheater=yes AltPalette=yes ; Damaged missile silo launch anim [NAMISL_AD] Image=NAMISL_A Normalized=yes Start=11 LoopStart=11 LoopEnd=22 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes [NAMISL_B] Image=NAMISL_B Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes [NAMISL_BD] Image=NAMISL_B Normalized=yes Start=10 LoopStart=10 LoopEnd=20 LoopCount=-1 Rate=220 ;NewTheater=yes Surface=yes [GAFIRE_B] Image=GAFIRE_B Normalized=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=500 Surface=yes NewTheater=yes [GAFIRE_C] Image=GAFIRE_C Normalized=yes LoopStart=0 LoopEnd=6 LoopCount=-1 Rate=220 Surface=yes NewTheater=yes [NAPOST_A] Image=NAPOST_A Normalized=yes LoopStart=0 LoopEnd=12 LoopCount=-1 Rate=220 Surface=yes NewTheater=yes DetailLevel=1 [NAPOST_AD] Image=NAPOST_A Normalized=yes Start=12 LoopStart=12 LoopEnd=24 LoopCount=-1 Rate=150 Surface=yes ;NewTheater=yes DetailLevel=1 [NAPOST_B] Image=NAPOST_B Normalized=yes LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=220 Surface=yes NewTheater=yes DetailLevel=1 ; Waterfall animation [WA01X] Theater=yes Normalized=yes LoopStart=1 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WA02X] Theater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WA03X] Theater=yes Normalized=yes LoopStart=1 ;was 0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WA04X] Theater=yes Normalized=yes LoopStart=1 ;was 0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WB01X] Theater=yes Normalized=yes LoopStart=1 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WB02X] Theater=yes Normalized=yes LoopStart=1 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WB03X] Theater=yes Normalized=yes LoopStart=1 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WB04X] Theater=yes Normalized=yes LoopStart=1 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WC01X] Theater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WC02X] Theater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WC03X] Theater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WC04X] Theater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WD01X] Theater=yes Normalized=yes LoopStart=1 ;was 0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WD02X] Theater=yes Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WD03X] Theater=yes Normalized=yes LoopStart=1 ;was 0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Waterfall animation [WD04X] Theater=yes Normalized=yes LoopStart=1 ;was 0 LoopEnd=8 LoopCount=-1 Rate=220 Flat=yes DetailLevel=2 DemandLoad=true ShouldUseCellDrawer=true ; Crashed scrin fighter/UFO ;[UFO] ;Theater=yes ;Normalized=yes ;LoopStart=0 ;LoopEnd=0 ;LoopCount=-1 ;Rate=0 ;Flat=yes ;DetailLevel=0 ;DemandLoad=true ; Tiberium silo fill animation [GASILO_A] Image=GASILO_A Normalized=yes Rate=0 Surface=yes NewTheater=yes [GASILO_AD] Image=GASILO_A Normalized=yes Start=4 Rate=0 Surface=yes NewTheater=yes ; Tiberium silo active animation [GASILO_B] Image=GASILO_B Normalized=yes LoopStart=0 LoopEnd=16 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 [GASILO_BD] Image=GASILO_B Normalized=yes Start=16 LoopStart=16 LoopEnd=32 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 [GASPOT_A] Image=GASPOT_A Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=1 [GASPOT_AD] Image=GASPOT_A Normalized=yes Start=8 LoopStart=8 LoopEnd=16 LoopCount=-1 Rate=220 Surface=yes ;NewTheater=yes DetailLevel=1 [CTDAM_A] Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 ;TerrainPalette=yes ;NewTheater=yes ;DemandLoad=true [CTDAM_AD] Image=CTDAM_A Normalized=yes Start=10 LoopStart=10 LoopEnd=20 LoopCount=-1 Rate=220 ;TerrainPalette=yes ;NewTheater=yes ;DemandLoad=true [CTDAM_B] Normalized=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=350 Surface=yes ;TerrainPalette=yes ;NewTheater=yes ;DemandLoad=true [CTDAM_BD] Image=CTDAM_B Normalized=yes Start=8 LoopStart=8 LoopEnd=16 LoopCount=-1 Rate=350 Surface=yes ;TerrainPalette=yes ;NewTheater=yes ;DemandLoad=true ; Tunnel roof [TUNTOP01] Theater=yes Normalized=yes Surface=yes YSortAdjust=1000 LoopStart=0 LoopEnd=0 LoopCount=-1 Rate=0 Flat=yes DetailLevel=0 DemandLoad=true ShouldFogRemove=false ; Tunnel roof [TUNTOP02] Theater=yes Surface=yes YSortAdjust=1000 Normalized=yes LoopStart=0 LoopEnd=0 LoopCount=-1 Rate=0 Flat=yes DetailLevel=0 DemandLoad=true ShouldFogRemove=false ; Tunnel roof [TUNTOP03] Theater=yes Normalized=yes LoopStart=0 LoopEnd=0 LoopCount=-1 Rate=0 Flat=yes DetailLevel=0 DemandLoad=true ShouldFogRemove=false ; Tunnel roof [TUNTOP04] Theater=yes Normalized=yes LoopStart=0 LoopEnd=0 LoopCount=-1 Rate=0 Flat=yes DetailLevel=0 DemandLoad=true ShouldFogRemove=false ; Larger meteor [METLARGE] Elasticity=0.0 MaxXYVel=100.0 MinZVel=-50.0 ExpireAnim=TWLT070 Damage=5000000 DamageRadius=300 Warhead=Meteorite IsMeteor=true Spawns=METDEBRI SpawnCount=5 LoopStart=0 LoopEnd=8 LoopCount=-1 RandomRate=220,500 DetailLevel=0 TrailerAnim=SMOKEY2 TrailerSeperation=1 Report=METEOR1 ; Small meteor [METSMALL] Elasticity=0.0 MinZVel=-50.0 MaxXYVel=100.0 ExpireAnim=TWLT100 Damage=5000000 DamageRadius=300 Warhead=Meteorite IsMeteor=true IsTiberium=true Spawns=METDEBRI SpawnCount=7 LoopStart=0 LoopEnd=8 LoopCount=-1 RandomRate=220,500 DetailLevel=0 TrailerAnim=METSTRAL TrailerSeperation=1 Report=METEOR2 ; Meteor impact debris [METDEBRI] Elasticity=0.0 MinZVel=40.0 MaxXYVel=18.0 ExpireAnim=TWLT070 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=220,500 Bouncer=yes ;TiberiumRadius=1 TiberiumSpawnType=TIB01 Report=METHIT1 ;Meteor smoke trail [METSTRAL] LoopStart=0 LoopEnd=8 LoopCount=1 Rate=600 DetailLevel=1 Next=SMOKEY ; Meteor trail [METLTRAL] LoopStart=0 LoopEnd=8 LoopCount=1 Rate=600 DetailLevel=1 Next=SMOKEY [CRYSTAL1] Elasticity=0.0 MinZVel=40.0 MaxXYVel=18.0 ExpireAnim=TWLT026 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=220,500 Bouncer=yes Theater=yes TiberiumSpreadRadius=0 TiberiumSpawnType=TIB2_01 [CRYSTAL2] Elasticity=0.0 MinZVel=40.0 MaxXYVel=18.0 ExpireAnim=TWLT026 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=220,500 Bouncer=yes Theater=yes TiberiumSpreadRadius=0 TiberiumSpawnType=TIB2_01 [CRYSTAL3] Elasticity=0.0 MinZVel=40.0 MaxXYVel=18.0 ExpireAnim=TWLT026 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=220,500 Bouncer=yes Theater=yes TiberiumSpreadRadius=0 TiberiumSpawnType=TIB2_01 [CRYSTAL4] Elasticity=0.0 MinZVel=40.0 MaxXYVel=18.0 ExpireAnim=TWLT026 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true LoopStart=0 LoopEnd=14 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=220,500 Bouncer=yes Theater=yes TiberiumSpreadRadius=0 TiberiumSpawnType=TIB2_01 AnimLow=0 AnimHigh=14 Voxel=no [BIGBLUE] Image=BIGBLUE3 LoopStart=0 LoopEnd=9 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=150,250 ;Theater=yes IsAnimatedTiberium=yes Surface=yes YDrawOffset=-52 AltPalette=yes UseNormalLight=yes [BIGBLUE3] ;Theater=yes Foundation=1x1 AltPalette=yes LoopStart=0 LoopEnd=9 LoopCount=-1 Rate=500 DetailLevel=0 RandomRate=150,250 Surface=yes YDrawOffset=-16 UseNormalLight=yes [FLAMEGUY] IsFlamingGuy=true RunningFrames=6 LoopCount=1 Rate=500 ; This is a tricky one. It's an animation AND a projectile all in the same section ; The anim stuff is first then the projectile stuff. [PULSBALL] Start=0 End=8 LoopCount=1 Rate=220 DetailLevel=0 AnimPalette=yes AnimLow = 8 AnimHigh = 22 AnimRate = 1 ; Deployable sensor array [GADPSA_A] Normalized=yes Rate=220 Surface=yes NewTheater=yes LoopCount=-1 ; Deployable ICBM launcher [GAICBM_A] Rate=0 Surface=yes NewTheater=yes LoopCount=-1 ; Civilian array [CAARAY_A] Image=CAARAY_A Normalized=yes LoopStart=0 LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=0 LoopEnd=16 DemandLoad=true [CAARAY_B] Image=CAARAY_B Normalized=yes LoopStart=0 LoopEnd=16 DemandLoad=true LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=0 [CAARAY_C] Image=CAARAY_C Normalized=yes LoopStart=0 LoopEnd=16 DemandLoad=true LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=2 [CAARAY_CD] Image=CAARAY_C Normalized=yes LoopStart=16 LoopEnd=32 DemandLoad=true LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=2 [CAARAY_D] Image=CAARAY_D Normalized=yes LoopStart=0 LoopEnd=12 DemandLoad=true LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=2 [CAARAY_DD] Image=CAARAY_D Normalized=yes Start=12 LoopStart=12 LoopEnd=24 DemandLoad=true LoopCount=-1 Rate=350 Surface=yes NewTheater=yes DetailLevel=2 [CARYLAND] Image=PODRING Normalized=no Rate=900 Surface=yes DetailLevel=1 Translucent=yes Flat=yes ;NormalZAdjust=1 YSortAdjust=-2000 [DROPLAND] Image=PODRING Normalized=no Rate=180 Surface=yes DetailLevel=1 Translucent=yes Flat=yes ;NormalZAdjust=1 YSortAdjust=-200 ;============================================================================ ; FIRESTORM ADDITIONS & CHANGES ;============================================================================ [FONA01] Theater=yes Foundation=1x1 [FONA02] Theater=yes Foundation=1x1 [FONA03] Theater=yes Foundation=1x1 [FONA04] Theater=yes Foundation=1x1 [FONA05] Theater=yes Foundation=1x1 [FONA06] Theater=yes Foundation=1x1 [FONA07] Theater=yes Foundation=1x1 [FONA08] Theater=yes Foundation=1x1 [FONA09] Theater=yes Foundation=1x1 [FONA10] Theater=yes Foundation=1x1 [FONA11] Theater=yes Foundation=1x1 [FONA12] Theater=yes Foundation=1x1 [FONA13] Theater=yes Foundation=1x1 [FONA14] Theater=yes Foundation=1x1 [FONA15] Theater=yes Foundation=1x1 ; Infantry struggle under web [WEBGUY] Normalized=true AltPalette=false Flat=yes Surface=yes RandomLoopDelay=10,300 [MEMPFX] Normalized=yes Surface=yes Translucent=yes UseNormalLight=yes ; Deployed limpet mine [DLIMPET] Foundation=1x1 Height=1 Buildup=DLIMPMK AnimActive=0,10,3 ActiveAnim=DLIMP_A [DLIMP_A] Image=DLIMP_A Normalized=yes LoopStart=0 LoopEnd=10 LoopCount=-1 Rate=350 Surface=yes NewTheater=no DetailLevel=1 ;DemandLoad=true ; Kodiak Crash [C_KODIAK] Foundation=3x3 TerrainPalette=yes ExtraDamageStage=false ;DemandLoad=true Height=3 AnimActive=0,20,3 ActiveAnim=K_LIGHT1 ActiveAnimX=-23 ActiveAnimY=0 ActiveAnimZAdjust=-100 ActiveAnimTwo=K_LIGHT2 ActiveAnimTwoX=144 ActiveAnimTwoY=144 ActiveAnimTwoZAdjust=-100 [K_LIGHT1] Image=K_LIGHT1 Normalized=yes LoopStart=0 LoopEnd=20 LoopCount=-1 Rate=350 Surface=no ;yes NewTheater=no DetailLevel=1 ;DemandLoad=true [K_LIGHT2] Image=K_LIGHT2 Normalized=yes LoopStart=0 LoopEnd=20 LoopCount=-1 Rate=350 Surface=no ;yes NewTheater=no DetailLevel=1 ;DemandLoad=true ; Mobile EMP [M_EMP] Cameo=MEMPICON Remapable=yes Voxel=yes ; Mobile Stealth Generator [SGEN] Cameo=MSTLICON Remapable=yes Voxel=yes ; Mobile Weapons Factory [MWAR_NOD] Cameo=MWARICON Remapable=yes Voxel=yes ; Juggernaught [JUGGER] Cameo=JUGGICON Voxel=no Remapable=yes WalkFrames=15 StandingFrames=0 Facings=8 [LIMPED] Cameo=LIMPICON ; Mobile Weapons Factory [MWAR] Remapable=yes Foundation=4x3 Height=2 NormalZAdjust=-10 AnimActive=0,1,0 Buildup=MWARMK ;DemandLoadBuildup=true ;FreeBuildup=true DeployingAnim=MWAR_2 DoorAnim=MWAR_D DoorStages=12 UnderDoorAnim=MWAR_1 ;NewTheater=yes BibShape=MWARBB ActiveAnim=MWAR_A ActiveAnimZAdjust=-119 ActiveAnimTwo=MWAR_B ActiveAnimTwoZAdjust=-119 ActiveAnimThree=MWAR_C ActiveAnimThreeZAdjust=-119 ; Mobile Weapons Factory (Overlay A) [MWAR_A] Normalized=yes ;NewTheater=yes LoopStart=0 LoopEnd=5 LoopCount=-1 Rate=400 Surface=yes DetailLevel=1 ; Mobile Weapons Factory (Overlay B) [MWAR_B] Normalized=yes ;NewTheater=yes LoopStart=0 LoopEnd=12 LoopCount=-1 Rate=400 Surface=yes DetailLevel=1 ; Mobile Weapons Factory (Overlay C) [MWAR_C] Normalized=yes ;NewTheater=yes LoopStart=0 LoopEnd=8 LoopCount=-1 Rate=800 Surface=yes DetailLevel=1 ; Deployed Juggernaut [DJUGG] Normalized=yes Remapable=yes Foundation=1x1 Buildup=DJUGGMK Height=1 PBarrelLength=224 PrimaryFireFLH=0,0,64 TurretNotExportedOnGround=yes ; Deployed Mobile Stealth Generator [MSTL] Remapable=yes Normalized=yes Foundation=1x1 Buildup=MSTLMK ;DemandLoadBuildup=true Height=1 ;FreeBuildup=true ActiveAnim=MSTL_A ExtraLight=-100 ; Deployed Mobile Stealth Generator (Overlay A) [MSTL_A] Normalized=yes Rate=220 Surface=yes LoopCount=-1 [DEFENDER] WalkFrames=8 FiringFrames=12 Voxel=no Remapable=no PrimaryFireFLH=200,-200,450 SecondaryFireFLH=200,200,450 FiringSyncFrame1=8 FiringSyncFrame2=3 StartStandFrame=0 StartWalkFrame=8 StartFiringFrame=72 Facings=8 ; Deployed Core Defender [DEFD] Normalized=yes Remapable=yes Foundation=2x2 Height=1 Buildup=DEFDMK ;DemandLoadBuildup=true ; Core defender explosion [DEFD_EXP] Normalized=yes Rate=350 Report=EXPNEW05 Next=TWLT100 ; Cabals Core [CORE] Foundation=3x3 Height=3 Buildup=COREMK ;DemandLoadBuildup=true ExtraDamageStage=yes ActiveAnim=CORE_A ActiveAnimDamaged=CORE_AD ActiveAnimZAdjust=-100 ActiveAnimTwo=CORE_B ActiveAnimTwoDamaged=CORE_BD ActiveAnimTwoZAdjust=-100;-25 ActiveAnimThree=CORE_C ActiveAnimThreeDamaged=CORE_CD ActiveAnimThreeZAdjust=-100;-50 [CORE_A] Normalized=yes Start=0 LoopStart=0 LoopEnd=60 LoopCount=-1 Surface=yes NewTheater=no DetailLevel=1 Rate=200 [CORE_AD] Image=CORE_A Normalized=yes Start=60 LoopStart=60 LoopEnd=120 LoopCount=-1 NewTheater=no Surface=yes DetailLevel=1 Rate=200 [CORE_B] Normalized=yes Start=0 LoopStart=0 LoopEnd=20 LoopCount=-1 Surface=yes NewTheater=no DetailLevel=1 Rate=200 [CORE_BD] Image=CORE_B Normalized=yes Start=20 LoopStart=20 LoopEnd=40 LoopCount=-1 NewTheater=no Surface=yes DetailLevel=1 Rate=200 [CORE_C] Normalized=yes Start=0 LoopStart=0 LoopEnd=30 LoopCount=-1 Surface=yes NewTheater=no DetailLevel=1 Rate=200 [CORE_CD] Image=CORE_C Normalized=yes Start=30 LoopStart=30 LoopEnd=60 LoopCount=-1 NewTheater=no Surface=yes DetailLevel=1 Rate=200 ; Cyborg reaper [REAPER] Cameo=REAPICON Facings=8 WalkFrames=12 StandingFrames=1 FiringFrames=0 ;DeathFrames=13 ;DeathFrameRate=3 StartWalkFrame=8 StartStandFrame=0 ;StartDeathFrame=104 ;MaxDeathCounter=64 PrimaryFireFLH=50,110,100 SecondaryFireFLH=0,0,230 [REAPRDIE] Normalized=yes End=13 Rate=350 AltPalette=yes Report=SPIDDIE1 Next=S_TUMU60 ;TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 [OBL1] Remapable=yes Foundation=2x2 ChargeAnim=yes ;Buildup= Height=3 DemandLoadBuildup=true FreeBuildup=true PrimaryFirePixelOffset=2,-38 ;NewTheater=yes ;AnimActive=0,24,3 ActiveAnim=OBL1_A ActiveAnimDamaged=OBL1_AD ActiveAnimZAdjust=-100 ActiveAnimTwo=OBL1_B ActiveAnimTwoDamaged=OBL1_BD ActiveAnimTwoZAdjust=-100 [OBL1_A] Image=OBL1_A Normalized=yes Surface=yes Start=0 LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=200 DetailLevel=1 [OBL1_AD] Image=OBL1_A Normalized=yes Surface=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=200 DetailLevel=1 [OBL1_B] Image=OBL1_B Normalized=yes Surface=yes Start=0 LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=200 DetailLevel=1 [OBL1_BD] Image=OBL1_B Normalized=yes Surface=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=200 DetailLevel=1 [OBL1_C] Image=OBL1_C Normalized=yes Surface=yes Start=0 LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=200 DetailLevel=1 [OBL1_CD] Image=OBL1_C Normalized=yes Surface=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=200 DetailLevel=1 [OBL2] Normalized=yes Remapable=yes Foundation=1x2 ChargeAnim=yes ;Buildup= Height=4 DemandLoadBuildup=true FreeBuildup=true PrimaryFirePixelOffset=-20,-76 ;AnimActive=0,24,3 ActiveAnim=OBL2_A ActiveAnimDamaged=OBL2_AD ActiveAnimZAdjust=-100 ActiveAnimTwo=OBL2_B ActiveAnimTwoDamaged=OBL2_BD ActiveAnimTwoZAdjust=-3 [OBL2_A] Image=OBL2_A Normalized=yes Surface=yes Start=0 LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=200 DetailLevel=1 [OBL2_AD] Image=OBL2_A Normalized=yes Surface=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=200 DetailLevel=1 [OBL2_B] Image=OBL2_B Normalized=yes Surface=yes Start=0 LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=200 DetailLevel=1 [OBL2_BD] Image=OBL2_B Normalized=yes Surface=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=200 DetailLevel=1 [OBL2_C] Image=OBL2_C Normalized=yes Surface=yes Start=0 LoopStart=0 LoopEnd=24 LoopCount=-1 Rate=200 DetailLevel=1 [OBL2_CD] Image=OBL2_C Normalized=yes Surface=yes Start=24 LoopStart=24 LoopEnd=48 LoopCount=-1 Rate=200 DetailLevel=1 ; *** Movies *** ; Each of the movies allowed in the game will be listed ; here. [Movies] 00=CAP_TRAT 01=COUP 02=VEGAWIN 03=DISKDEST 04=INTRO 05=GDI_M02 06=GDI_M03 07=GDI_M04 08=GDI_M05 09=GDI_M06 10=GDI_M07 11=GDI_M08 12=GDI_M09A 13=GDI_M09B 14=GDI_M09C 15=GDI_M10A 16=GDIM09CW 17=GDI_M11 18=GDI_M12A 19=HIDESEEK 20=ICESKATE 21=MECHATAK 22=EVA 23=NOD_M02 24=NOD_M03 25=NOD_M04 26=NOD_M06 27=NOWCNOT 28=ORCASTRK 29=PODASSLT 30=RETRBTN 31=TENEVICT 32=TRAINROB 33=NOD06ABW 34=EMPULSE 35=NOD_M09 36=STARTUP 37=ICBMLNCH 38=BEACHEAD 39=GDI_FINL 40=NOD_M05 41=GENNODL1 42=GDIM09D1 43=GDI01_SB 44=GDI02_SB 45=GDI03_SB 46=NOD_M07 47=NOD_M08 48=NOD_M10 49=NOD_M11 50=NOD_M12 51=NOD_FINL 52=NOD01_SB 53=NOD02_SB 54=GENWIN01 55=UFOGUARD 56=WWLOGO 57=KILL_GDI 58=KILLMECH 59=UNSTPBLE 60=N_LOGO_W 61=N_LOGO_L 62=NOD_FLAG 63=GDI_LOGO 64=GDI_FLAG 65=DAMBREAK 66=FSGDIM02 ; Firestorm movies start here 67=FSGDIM03 68=FSGDIM07 69=FSNODM02 70=FSNODM06 71=FS_TITLE 72=FSNODM01 73=FSNODM03 74=FSNODM04 75=FSNODM07 76=FSNODM09 77=FSNODM05 78=FSNODM08 79=MEKATAK2 80=FSGDIM04 81=FSGDIM05 82=FSGDIM06 83=FSGDIM08 84=FSGDIM09 85=FSGDIFNL 86=FSGDIINT 87=FS_SB01 88=TS_TITLE 89=FSNODFNL 90=INTRON 91=MOBSTLTH 92=RAGDOLL 93=REAPBOMB ================================================ FILE: DXMainClient/Resources/INI/artfs.ini ================================================ ;============================================================================ ; FIRESTORM ADDITIONS & CHANGES ;============================================================================ [GAWALL] DamageLevels=2 [NAWALL] DamageLevels=2 ================================================ FILE: DXMainClient/Resources/INI/day.ini ================================================ [VariableNames] 0=Get Dark,1 1=Get Light,0 [Triggers] 09565DC0=Neutral,,Start Cycle,0,1,1,1,0 09565750=Neutral,,Nightfall,0,1,1,1,0 09565310=Neutral,,Daybreak,0,1,1,1,0 09F31930=Neutral,,Light On,0,1,1,1,0 09F31410=Neutral,09F31930,Light Off,0,1,1,1,0 [Events] 09565DC0=1,47,0,180 09565750=2,36,0,0,13,0,1800 09565310=2,36,0,1,13,0,1800 09F31930=1,45,0,60 09F31410=1,46,0,70 [Actions] 09565DC0=1,56,0,0,0,0,0,0,A 09565750=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,50,0,0,0,0,A,57,0,0,0,0,0,0,A,56,0,1,0,0,0,0,A 09565310=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,100,0,0,0,0,A,57,0,1,0,0,0,0,A,56,0,0,0,0,0,0,A 09F31930=3,62,0,0,0,0,0,0,A,54,2,09F31930,0,0,0,0,A,53,2,09F31410,0,0,0,0,A 09F31410=3,61,0,0,0,0,0,0,A,54,2,09F31410,0,0,0,0,A,53,2,09F31930,0,0,0,0,A [Tags] 09565870=0,Start Cycle,09565DC0 095653C0=2,Nightfall,09565750 09565060=2,Daybreak,09565310 09F314C0=2,Light On/Off,09F31410 ================================================ FILE: DXMainClient/Resources/INI/dusk.ini ================================================ [VariableNames] 0=Get Dark,1 1=Get Light,0 [Triggers] 09565DC0=Neutral,,Go to Night,0,1,1,1,0 09565750=Neutral,,Nightfall,0,1,1,1,0 09565310=Neutral,,Daybreak,0,1,1,1,0 09F31930=Neutral,,Light On,0,1,1,1,0 09F31410=Neutral,09F31930,Light Off,0,1,1,1,0 [Events] 09565DC0=1,47,0,180 09565750=2,13,0,1800,36,0,0 09565310=2,13,0,1800,36,0,1 09F31930=1,45,0,60 09F31410=1,46,0,70 [Actions] 09565DC0=4,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,50,0,0,0,0,A,56,0,1,0,0,0,0,A 09565750=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,50,0,0,0,0,A,57,0,0,0,0,0,0,A,56,0,1,0,0,0,0,A 09565310=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,100,0,0,0,0,A,57,0,1,0,0,0,0,A,56,0,0,0,0,0,0,A 09F31930=3,62,0,0,0,0,0,0,A,54,2,09F31930,0,0,0,0,A,53,2,09F31410,0,0,0,0,A 09F31410=3,61,0,0,0,0,0,0,A,54,2,09F31410,0,0,0,0,A,53,2,09F31930,0,0,0,0,A [Tags] 09565870=0,Go to Night,09565DC0 095653C0=2,Nightfall,09565750 09565060=2,Daybreak,09565310 09F314C0=2,Light On/Off,09F31410 ================================================ FILE: DXMainClient/Resources/INI/firestrm.ini ================================================ [General] Name=Tiberian Sun - Firestorm DropPodInfantryMinimum=5 ;was 3 DropPodInfantryMaximum=8 ;was 5 BallisticScatter=2.0 ;was 1.5 SurvivorRate=.1 ; was .4 SurvivorDivisor=100 ; veteran factors updated VeteranRatio=5.0 ; must destroy this multiple of self-value to become a veteran [per level] VeteranCombat=.50 ; combat BONUS factor when unit is a veteran VeteranSpeed=.30 ; speed BONUS factor when unit is a veteran VeteranSight=0.0 ; sight range BONUS when unit is a veteran VeteranArmor=.50 ; armor BONUS when unit is a veteran VeteranROF=.30 ; rate of fire BONUS when unit is a veteran VeteranCap=2 ; maximum veteran level that can be obtained InitialVeteran=no ; Do initial forces start as veterans? [JumpjetControls] CloakDetectionRadius=3 ;============================================================================ ; VEHICLES / UNITS ;============================================================================ [REAPER] TechLevel=6 AllowedToStartInMultiplayer=yes [JUGG] TechLevel=6 AllowedToStartInMultiplayer=yes [LIMPET] TechLevel=3 [MOBILEMP] TechLevel=6 IsMobileEMP=true ;resets if not redefined [SGEN] TechLevel=9 [MOBWARG] TechLevel=10 [MOBWARN] TechLevel=10 [SONIC] Speed=5 EliteAbilities=FASTER Accelerates=false [STNK] Strength=200 ; w180 Sight=7 [TRUCKA] Crewed=no [TRUCKB] Crewed=no ;============================================================================ ; AIRCRAFT ;============================================================================ [SCRIN] Cost=1250 ;was 1500 [APACHE] Cost=800 ;was 1000 ;============================================================================ ; INFANTRY ;============================================================================ [JUMPJET] Speed=8 [E2] CollateralDamageCoefficient=.33 ;============================================================================ ; BUILDINGS ;============================================================================ [GAPLUG4] TechLevel=10 AIBuildThis=yes [CA0016] Name=D's Dog House ;============================================================================ ; WEAPON / PROJECTILE ;============================================================================ [DropGun] Damage=1 ;was 50 ;Fix to make Artillery less accurate [Ballistic] High=yes Image=120MM Arcing=true Bouncy=yes Elasticity=0.0 [SlimeAttack] Range=2.0 [Grenade] ROF=80 ;was 60 [Bomb] Range=3 ;was 5 ;============================================================================ ; TERRIAN OVERLAYS ;============================================================================ [TerrainTypes] 1=GAWALL 2=NAWALL [GAWALL] Strength=225 [NAWALL] Strength=225 ================================================ FILE: DXMainClient/Resources/INI/ion.ini ================================================ [General] IonLightningFrequency=10 IonLightningRandomness=90 IonLightningDamage=200 IonStormWarning=33 [Lighting] IonAmbient=0.500000 IonRed=1.620000 IonGreen=1.250000 IonBlue=0.340000 IonGround=0.000000 IonLevel=0.000000 [Triggers] 07709990=Neutral,,Ion Storm,0,1,1,1,0 [Events] 07709990=1,51,0,1800 [Actions] 07709990=1,44,0,240,0,0,0,0,A [Tags] 07709670=2,Ion Storm,07709990 ================================================ FILE: DXMainClient/Resources/INI/keyboard.ini ================================================ [Hotkey] ChatToAllies=8 CenterView=12 ChatToAll=13 ShowHelp=20 Options=27 ToggleRadar=9 CenterOnRadarEvent=32 RightSidebarUp=33 RightSidebarDown=34 LeftSidebarDown=35 LeftSidebarUp=36 TeamSelect_10=48 TeamSelect_1=49 TeamSelect_2=50 TeamSelect_3=51 TeamSelect_4=52 TeamSelect_5=53 TeamSelect_6=54 TeamSelect_7=55 TeamSelect_8=56 TeamSelect_9=57 ToggleAlliance=65 PreviousObject=66 DeployObject=68 SelectView=69 Follow=70 GuardObject=71 CenterBase=72 NextObject=78 TogglePower=80 ToggleRepair=82 StopObject=83 SelectType=84 WaypointMode=87 ScatterObject=88 PlaceBuilding=90 PageUser=106 DeleteWaypoint=110 View1=120 View2=121 View3=122 View4=123 ToggleInfoPanel=192 TeamAddSelect_10=304 TeamAddSelect_1=305 TeamAddSelect_2=306 TeamAddSelect_3=307 TeamAddSelect_4=308 TeamAddSelect_5=309 TeamAddSelect_6=310 TeamAddSelect_7=311 TeamAddSelect_8=312 TeamAddSelect_9=313 ToggleSell=594 RepeatBuilding=602 TeamCreate_10=560 TeamCreate_1=561 TeamCreate_2=562 TeamCreate_3=563 TeamCreate_4=564 TeamCreate_5=565 TeamCreate_6=566 TeamCreate_7=567 TeamCreate_8=568 TeamCreate_9=569 ScreenCapture=579 SetView1=632 SetView2=633 SetView3=634 SetView4=635 TeamCenter_10=1072 TeamCenter_1=1073 TeamCenter_2=1074 TeamCenter_3=1075 TeamCenter_4=1076 TeamCenter_5=1077 TeamCenter_6=1078 TeamCenter_7=1079 TeamCenter_8=1080 TeamCenter_9=1081 ScrollWest=2085 ScrollNorth=2086 ScrollEast=2087 ScrollSouth=2088 ;ForceWin=1111 ;BailOut=1112 ;SidebarPageUp=2085 ;SidebarUp=2086 ;SidebarPageDown=2087 ;SidebarDown=2088 ================================================ FILE: DXMainClient/Resources/INI/morning.ini ================================================ [VariableNames] 0=Get Dark,1 1=Get Light,0 [Triggers] 09565DC0=Neutral,,Go to Day,0,1,1,1,0 09565750=Neutral,,Nightfall,0,1,1,1,0 09565310=Neutral,,Daybreak,0,1,1,1,0 09F31930=Neutral,,Light On,0,1,1,1,0 09F31410=Neutral,09F31930,Light Off,0,1,1,1,0 [Events] 09565DC0=1,47,0,180 09565750=2,36,0,0,13,0,1800 09565310=2,36,0,1,13,0,1800 09F31930=1,45,0,60 09F31410=1,46,0,70 [Actions] 09565DC0=4,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,100,0,0,0,0,A,56,0,0,0,0,0,0,A 09565750=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,50,0,0,0,0,A,57,0,0,0,0,0,0,A,56,0,1,0,0,0,0,A 09565310=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,100,0,0,0,0,A,57,0,1,0,0,0,0,A,56,0,0,0,0,0,0,A 09F31930=3,62,0,0,0,0,0,0,A,54,2,09F31930,0,0,0,0,A,53,2,09F31410,0,0,0,0,A 09F31410=3,61,0,0,0,0,0,0,A,54,2,09F31410,0,0,0,0,A,53,2,09F31930,0,0,0,0,A [Tags] 09565870=0,Go to Day,09565DC0 095653C0=2,Nightfall,09565750 09565060=2,Daybreak,09565310 09F314C0=2,Light On/Off,09F31410 ================================================ FILE: DXMainClient/Resources/INI/night.ini ================================================ [VariableNames] 0=Get Dark,1 1=Get Light,0 [Triggers] 09565DC0=Neutral,,Start Cycle,0,1,1,1,0 09565750=Neutral,,Nightfall,0,1,1,1,0 09565310=Neutral,,Daybreak,0,1,1,1,0 09F31930=Neutral,,Light On,0,1,1,1,0 09F31410=Neutral,09F31930,Light Off,0,1,1,1,0 [Events] 09565DC0=1,47,0,180 09565750=2,13,0,1800,36,0,0 09565310=2,13,0,1800,36,0,1 09F31930=1,45,0,60 09F31410=1,46,0,70 [Actions] 09565DC0=1,56,0,1,0,0,0,0,A 09565750=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,50,0,0,0,0,A,57,0,0,0,0,0,0,A,56,0,1,0,0,0,0,A 09565310=5,71,0,1032805416,0,0,0,0,A,72,0,1061997772,0,0,0,0,A,73,0,100,0,0,0,0,A,57,0,1,0,0,0,0,A,56,0,0,0,0,0,0,A 09F31930=3,62,0,0,0,0,0,0,A,54,2,09F31930,0,0,0,0,A,53,2,09F31410,0,0,0,0,A 09F31410=3,61,0,0,0,0,0,0,A,54,2,09F31410,0,0,0,0,A,53,2,09F31930,0,0,0,0,A [Tags] 09565870=0,Start Cycle,09565DC0 095653C0=2,Nightfall,09565750 09565060=2,Daybreak,09565310 09F314C0=2,Light On/Off,09F31410 ================================================ FILE: DXMainClient/Resources/INI/rules.ini ================================================ ; RULE*.INI - update8 ; *** Tiberian Sun Rules *** ; If placed in game directory, it will override built in values. Values to be used as multipliers ; or percentages can be specified as either a simple floating point number (embed ".") or as a ; conventional percentage number (append "%"). Values used as distances or time delays ; are specified as simple floating point number. Distance values are expressed in cells. Time ; values are expressed in minutes. ; If multiple rules files are present, the Name field is used to identify between them. [General] Name=Tiberian Sun -- Official Rules of Engagement ; veteran factors VeteranRatio=10.0 ; must destroy this multiple of self-value to become a veteran [per level] VeteranCombat=.25 ; combat BONUS factor when unit is a veteran VeteranSpeed=.30 ; speed BONUS factor when unit is a veteran VeteranSight=0.0 ; sight range BONUS when unit is a veteran VeteranArmor=.25 ; armor BONUS when unit is a veteran VeteranROF=.20 ; rate of fire BONUS when unit is a veteran VeteranCap=2 ; maximum veteran level that can be obtained InitialVeteran=no ; Do initial forces start as veterans? ; repair and refit RefundPercent=50% ; percent of original cost to refund when building/unit is sold ReloadRate=.5 ; minutes to reload each ammo point for aircraft or helicopters RepairPercent=20% ; percent cost to fully repair as ratio of full cost RepairRate=.016 ; minutes between applying repair step RepairStep=8 ; hit points to heal per repair 'tick' URepairRate=.016 ; [units only] minutes between applying repair step UnitSelfHealRepairStep=1; amount of HP to increase unit's health with for every step while self-healing (def=1) IRepairRate=.001 ; [infantry only] minutes between applying repair step IRepairStep=1 ; [infantry only] hit points to heal per repair 'tick' for infantry TiberiumHeal=.010 ; minutes between applying Tiberium healing [for those units that heal in Tiberium] ; income and production ;BailCount=28 ; number of 'bails' carried by a harvester BuildSpeed=.8 ; general build speed [time (in minutes) to produce a 1000 credit cost item] BuildupTime=.06 ; average minutes that building build-up animation runs GrowthRate=5 ; minutes between ore (Tiberium) growth TiberiumGrows=yes ; Does ore grow denser over time? TiberiumSpreads=yes ; Does ore spread into adjacent areas? SeparateAircraft=yes ; Is first helicopter to be purchased separately from helipad? SurvivorRate=.4 ; fraction of building cost to be converted to survivors when sold SurvivorDivisor=100 ; the divisor into the survivor rate value to determine the number of survivors PlacementDelay=.05 ; delay before retrying produced object deploy if temporary blockage detected WeedCapacity=56 ; Amount of weed that needs to be harvested by a house in order to build the chem missile ;HarvesterDumpRate=0.0 ;HarvesterLoadRate=1.0 ; computer and movement controls CurleyShuffle=yes ; Should helicopter shuffle position between shots [as in C&C]? BaseBias=2 ; multiplier to threat target value when enemy is close to friendly base BaseDefenseDelay=.25 ; minutes delay between sending response teams to deal with base threat CloseEnough=2.25 ; If distance to destination less than this, then abort movement if otherwise blocked. DamageDelay=1 ; minutes between applying trivial structure damage when low on power GameSpeedBias=1 ; multiplier to overall game object movement speed Stray=2.0 ; radius distance (cells) that team members may stray without causing regroup action CloakDelay=.02 ; forced delay that subs will remain on surface before allowing to submerge SuspendDelay=2 ; minutes that suspended teams will remain suspended SuspendPriority=1 ; teams with less than this priority will suspend during base defense ops FlightLevel=600 ; typical flight level for aircraft [above ground level] MissileSpeedVar=.25 ; speed flucuation percentage that guided missiles have MissileROTVar=.25 ; rate of turn fluctuation percentage that guided missiles have TeamDelays=2250,2700,3600 ; interval between checking for and creating teams, by difficulty level AIHateDelays=5400,4500,4050 ; delay in frames before the computer chooses an enemy, by difficulty level AIAlternateProductionCreditCutoff=3000 ; when the AI house has less credits than this it will begin ; to spend money more conservatively NodAIBuildsWalls=no AIBuildsWalls=no MultiplayerAICM=250,200,100 HealScanRadius=10 ; how far should medic-type units scan for targets? Used to override the range ; of these units, because they need to have very short ranges FillEarliestTeamProbability=100,80,60 ; (by difficulty level, from hardest to easiest) MinimumAIDefensiveTeams=4,3,2 ; (by difficulty level, from hardest to easiest) MaximumAIDefensiveTeams=6,5,4 ; " " TotalAITeamCap=14,12,10 ; (by difficulty level, from hardest to easiest) UseMinDefenseRule=yes DissolveUnfilledTeamDelay=9000 ; how long to wait before dissolving an ai trigger team that has no members (multiplay only) LargeVisceroid=VISC_LRG ; when two small visceroids combine they turn into this SmallVisceroid=VISC_SML ; when infantry transmorgifies into a visceroid ; controls how the computer AI scores potential ion cannon targets ; the first value is for hard computer opponents, next for normal, and finally for easy ; right now, normal and hard are the same, because on hard, the computer will actually wait for ; production on an object to finish if that object is the best target; in this way all three ; difficulty levels are different. AIIonCannonConYardValue=100,100,100 AIIonCannonWarFactoryValue=50,50,50 AIIonCannonPowerValue=10,10,40 AIIonCannonEngineerValue=30,30,5 AIIonCannonThiefValue=20,20,5 AIIonCannonHarvesterValue=1,1,1 AIIonCannonMCVValue=150,150,20 AIIonCannonAPCValue=15,15,15 AIIonCannonBaseDefenseValue=35,35,35 AIIonCannonPlugValue=40,40,40 AIIonCannonHelipadValue=20,20,20 AIIonCannonTempleValue=40,40,40 ; Ion storm control IonLightningFrequency=10 ; Percent chance that lightning will strike this frame IonLightningRandomness=90 ; Percent chance that the lightning will strike a random cell instead of an object. IonLightningDamage=500 ; Damage done by lightning strike. IonStormDuration=120 ; Default ion storm duration in deconds. This is overriden by the trigger control. IonStormWarning=31 ; Warning time in seconds before an Ion Storm hits. IonStorms=no ; Are random ion storms going to appear? IonStormWarhead=IonWH ; Warhead used by ion storm strike. ; misc FogOfWar=no ; Is fog of war enabled? Visceroids=no ; Are randomly appearing visceroids going to occur? Meteorites=no ; Are tiberium meteorites going to occur? CrewEscape=50% ; percent chance that crew will escape from destroyed vehicle CameraRange=9 ; distance around spy camera to reveal map FineDiffControl=no ; Allow 5 difficulty settings instead of only 3 settings? Pilot=E1 ; pilot type that parachutes out of aircraft Crew=E1 ; soldier that emerges from destroyed unit or building Technician=CTECH ; civilian infantry type to serve as technician survivor [should be armed variety] Engineer=ENGINEER ; special (limited supply) infantry survivor from construction yards [probably engineer type] ;EngineerCaptureLevel=0.25 ;commented out because the Engineer's "enter" cursor disappears when the value is changed from 1.0 ;EngineerDamage=0.437 ;was 0.0 Disguise=E1 ; infantry type to appear as when disguised and viewed by the enemy Paratrooper=E1 ; infantry that is dropped as a paratrooper ; droppod flight characteristics DropPodWeapon=DropGun ; weapon mounted on drop pod DropPodHeight=2000 ; height above ground that drop pods appear at DropPodSpeed=75 ; speed of drop pod's descent DropPodAngle=0.79 ; angle of descent for drop pod [radians; .40=flat,1.18=steep] ; hover vehicle characteristics HoverHeight=120 ; height of hovering vehicles HoverDampen=40% ; dampening effect on hover vehicle bounciness HoverBob=.04 ; time between hover 'bobs' HoverBoost=150% ; hover speed when traveling on straight away HoverAcceleration=.02 ; time to accelerate to full speed HoverBrake=.03 ; time to decelerate to full stop ; subterrainean vehicle characteristics TunnelSpeed=1 ; production & power effects MultipleFactory=0 ; factory bonus for multiples [1=full bonus, 0=no bonus] (def=1) MinProductionSpeed=.5 ; minimum production speed as result of low power (def=.5) ; hack section GDIGateOne=GAGATE_A ; these buildings affect nearby walls, so I need to know what they are GDIGateTwo=GAGATE_B WallTower=GACTWR NodGateOne=NAGATE_A NodGateTwo=NAGATE_B NodRegularPower=NAPOWR NodAdvancedPower=NAAPWR GDIPowerPlant=GAPOWR GDIPowerTurbine=GAPOWRUP GDIHunterSeeker=GHUNTER NodHunterSeeker=NHUNTER GDIFirestormGenerator=GAFIRE RepairBay=GADEPT ; building to go to when in need of repairs PadAircraft=ORCA,ORCAB ; aircraft that can be produced (and land at) a helipad (or ground) ; AlexB's hack section BaseUnit=BASEUNIT ;whenever this unit appears, it's automatically swapped with the first MCV in the list below that belongs to the player's faction HarvesterUnit=HARV,MCV ;all harvesters that the AI can build, followed by all existing MCVs ;When building and replacing harvesters, the AI will build the first unit in this list that is owned by its faction and has Harvester=yes ;The first unit in this list without Harvester=yes -with the same owner as the player's faction- always replaces BASEUNIT (regardless of how it spawns) ;With exception of units with Harvester=yes, all units in this list are immune to Short Game (like MCVs normally are). ; Bret's hack section TreeStrength=200 ; 25 WindDirection=1 ; Direction of wind (gets converted to a FacingType, so 0 is north ; and increasing numbers rotate clockwise) TrackedUphill=.5 ; coefficient for tracked vehicle movement uphill TrackedDownhill=1.1 ; coefficient for tracked vehicle movement downhill WheeledUphill=.5 ; coefficient for wheeled vehicle movement uphill WheeledDownhill=1.2 ; coefficient for wheeled vehicle movement downhill LeptonsPerSightIncrease=2000 ;how high does a unit have to go before it can see farther? LeptonsPerFireIncrease=2000 ; how high does a unit have to go before it can fire farther? AttackingAircraftSightRange=6 BlendedFog=yes ; should we blend the fog (as opposed to dither it) CliffBackImpassability=2 ; how impassable is it behind cliffs? (0 = minimal, 2 = maximal) IceCrackingWeight=2.0 ; objects weighing more than this will crack ice IceBreakingWeight=4.0 ; objects weighing more than this well break through ice CloakingStages=9 TiberiumTransmogrify=40 TreeFlammability=.05 CraterLevel=1 ; controls how big the craters from meteorites are. ; 0 is no cratering, while 4 is the largest craters. ;StatisticTimeInterval=30; controls how many seconds pass between statistic calculations, for score screen graphs BridgeVoxelMax=3 ; maximum debris from each destroyed bridge section (def=3) WallBuildSpeedCoefficient=.5 ; how much faster than normal objects do walls build? WorstLowPowerBuildRateCoefficient=.3 ; what is the lowest the build rate can get for being low on power? BestLowPowerBuildRateCoefficient=.75 ; what is the highest the build rate can get when in a low power condition? ;ConditionYellowSparkingProbability=0.2 ;ConditionRedSparkingProbability=0.8 AllowShroudedSubteranneanMoves=true AircraftFogReveal=6 MaximumQueuedObjects=4 MaxWaypointPathLength=15 ;RevealByHeight=3 ; firestorm defense controls ChargeToDrainRatio=.333 DamageToFirestormDamageCoefficient=.1 ; veinhole monster parameters ; VeinholeMonsterStrength=1000 ; no longer used. To modify veinhole monster strength, edit the [VEINTREE] entry VeinholeGrowthRate=300 ; was 3000 VeinholeShrinkRate=100 ; was 500 MaxVeinholeGrowth=2000 VeinDamage=5 VeinholeTypeClass=VEINTREE ; AI trigger weighting parameters AITriggerSuccessWeightDelta=5 AITriggerFailureWeightDelta=-5 AITriggerTrackRecordCoefficient=1 ; Some spotlight controls SpotlightSpeed=.015 ; speed in radians SpotlightMovementRadius=2000 ; offset of center of arc sweep SpotlightLocationRadius=1000 ; offset from building SpotlightAcceleration=.0025 ; acceleration in radians SpotlightAngle=.5 ; maximum suggest angle of arc sweep ; Controls for radar events ; The events, in order, are: ; (1) Generic Combat Event, ; (2) Generic Noncombat Event, ; (3) Dropzone Event, ; (4) Base Under Attack Event, ; (5) Harvester Under Attack Event, ; (6) Enemy Object Sensed Event ; So, for example, to change the visibility duration of the Harvester Under Attack Event, ; you would change the fifth number in the list for RadarEventVisibilityDurations ; RadarEventSuppressionDistances=8, 8, 8, 8, 8, 6 ; suppression distance in cells RadarEventVisibilityDurations=200,200,200,200,200,200 ; event visibility in frames RadarEventDurations=400,400,400,400,400,400 ; event duration in frames FlashFrameTime=7 RadarCombatFlashTime=49 ; this should ALWAYS be an odd multiple of FlashFrameTime, ie RadarCombatFlashTime / FlashFrameTime should be an odd number RadarEventMinRadius=8 RadarEventSpeed=1.2 RadarEventRotationSpeed=.05 RadarEventColorSpeed=.1 RevealTriggerRadius=9 ; the sight range of a "reveal around waypoint" trigger, 10 is maximum ; id holders for particle systems and voxel debris ExplosiveVoxelDebris=GASTANK,PIECE ; name of explosive voxel debris TireVoxelDebris=TIRE ; name of tire voxel debris ScrapVoxelDebris=PIECE ; name of scrap metal voxel debris OKBuildingSmokeSystem=SmokeStackSys DamagedBuildingSmokeSystem=SmallSmokeSys DamagedUnitSmokeSystem=VSSmokeSys DebrisSmokeSystem=VSSmokeSys ; Building prerequisite categories are specified here. PrerequisitePower=GAPOWR,NAPOWR,NAAPWR PrerequisiteFactory=GAWEAP,NAWEAP,DGWEAP,DNWEAP PrerequisiteGDIFactory=GAWEAP,DGWEAP PrerequisiteNodFactory=NAWEAP,DNWEAP PrerequisiteBarracks=NAHAND,GAPILE PrerequisiteRadar=GARADR,NARADR PrerequisiteTech=GATECH,NATECH ; hunter seeker controls HunterSeekerDetonateProximity=150 HunterSeekerDescendProximity=700 HunterSeekerAscentSpeed=40 HunterSeekerDescentSpeed=50 HunterSeekerEmergeSpeed=6 ; default threat evaluation controls MyEffectivenessCoefficientDefault=200 TargetEffectivenessCoefficientDefault=-200 TargetSpecialThreatCoefficientDefault=200 TargetStrengthCoefficientDefault=-200 TargetDistanceCoefficientDefault=-1 ; -1 makes AI attack targettype (0,n) scripts to choose nearest enemy instead of first built ; defaults for dumb threat evaluation DumbMyEffectivenessCoefficient=200 DumbTargetEffectivenessCoefficient=200 DumbTargetSpecialThreatCoefficient=200 DumbTargetStrengthCoefficient=200 DumbTargetDistanceCoefficient=-1 EnemyHouseThreatBonus=400 ; ******* Jumpjet Flight rules ******* ; Jumpjet movement controls [JumpjetControls] TurnRate=4 Speed=14 Climb=5 CruiseHeight=500 ; cruiseheight should be higher than a bridge, just to be safe Acceleration=2 WobblesPerSecond=.15 ; was .25 WobbleDeviation=40 ; was 40 [LEVITATION] Drag=0.1 ; rate that jellyfish slows down ; max velocity that jellyfish can move again when... MaxVelocityWhenHappy=5.0 ; ...just puttering around MaxVelocityWhenFollowing=4.5 ; ...going someplace in particular MaxVelocityWhenPissedOff=10.0 ; ...tracking down some mofo AccelerationProbability=0.01 ; Chance happy jellyfish will "puff" AccelerationDuration=20 ; How long a puff accelerates the jellyfish Acceleration=0.75 ; How much a puff accelerates InitialBoost=2.0 ; How much of an initial speed boost does jellyfish get when puffing ;BounceVelocity=3.5 ; How fast does jellyfish bounce away after hitting a wall. Don't screw with this ;CollisionWaitDuration=15 ; How long does jellyfish wait before puffing after hitting a wall? MaxBlockCount=3 ; How many times will jellyfish block against a wall before giving up on destination? PropulsionSoundEffect=FLOATMOV,FLOTMOV2,FLOTMOV3,FLOTMOV4 ; Sound effect when puffing IntentionalDeacceleration=1.0 ; How fast does it deaccelerate when it wants to? (When going to waypoint or target) IntentionalDriftVelocity =12.0 ; How fast does it move when it is near its target? ProximityDistance=3.0 ; How close before special deacceleration & drift logic take over? ; ******* Special Weapon rules ******* ; Special weapon rules are specified here. [SpecialWeapons] HSBuilding=GAPLUG,NATMPL ; list of buildings the hunter seeker tries to pop out of NukeWarhead=Nuke ; warhead used by falling nuke missile NukeDown=NukeDown ; nuclear missile as it descends NukeProjectile=NukeUp ; nuclear missile (from silo) projectile to launch EMPulseWarhead=EMPuls ; warhead used by falling nuke missile EMPulseProjectile=PulsPr ; nuclear missile (from silo) projectile to launch ; ******* Audio / Visual rules ******* ; General controls that deal with audio or visual appearance of ; the game or the units therein are specified here. [AudioVisual] UnloadingHarvester=HORV ; obsolete harvester image to use when unloading tiberium. This now gets overruled by UndeploysInto= on Harvester units. PoseDir=2 ; aircraft landing facing (0=N, 1=NE, 2=E, etc) DropPodPuff=DROPEXP ; animation to play when drop pod hits the ground WaypointAnimationSpeed=10 ; how fast do the waypoint markers animate? BarrelExplode=EXPLOLRG ; exploding crates animation BarrelDebris=GASTANK,PIECE ; exploding crate debris list BarrelParticle=SmallGreySSys Wake=WAKE2 ; wake effect when traveling on/over water VeinAttack=VEINATAC DropPod=DROPPOD,DROPPOD2,DROPPODY,DROPPODY2 ; mark to leave after drop pod lands DeadBodies=DEATH_A,DEATH_B,DEATH_C,DEATH_D,DEATH_E,DEATH_F ; choice of dead bodies to leave around MetallicDebris=DBRIS1LG,DBRIS2LG,DBRIS3LG,DBRIS4LG,DBRIS5LG,DBRIS6LG,DBRIS7LG,DBRIS8LG,DBRIS9LG,DBRS10LG,DBRIS1SM,DBRIS2SM,DBRIS3SM,DBRIS4SM,DBRIS5SM,DBRIS6SM,DBRIS7SM,DBRIS8SM,DBRIS9SM,DBRS10SM BridgeExplosions=TWLT026,TWLT036,TWLT050,TWLT070 ; the explosions to use for the bridge explosion effect DigSound=SUBDRIL1 ; sound when digging into the ground Dig=DIG ; anim to play when unit digs into ground IonBlast=RING1 ; initial anim when ion cannon hits IonBeam=IONBEAM InfantryExplode=S_BANG34 ; animation when infantry just explodes AtmosphereEntry=PODRING ; animation to use when drop pod enters atmosphere GateUp=GATEUP1 ; sound of gate rising GateDown=GATEDWN1 ; sound of gate lowering ShroudGrow=no ; Does the shroud grow back over time? ScrollMultiplier=.07 ; multiplier to default scroll speed ShakeScreen=400 ; divide object strength by this to determine if the screen shakes when destroyed CloakSound=CLOAK5 ; sound of cloaking or decloaking SellSound=CASHTURN ; sound of selling objects (typically buildings) GameClosed=BLEEP1 ; game closed sound IncomingMessage=Message1 ; incoming message sound SystemError=BOOP ; system error sound OptionsChanged=Notify ; options have changed sound GameForming=GAMEFRM1 ; game forming sound PlayerLeft=BOOP ; player has left sound PlayerJoined=BOOP ; player has joined sound Construction=BOOP ; sound of building construction CreditTicks=CREDUP1,CREDDWN1 ; credit tick up and down sounds CrumbleSound=CRMBLE2 ; building crumble sound when building is completely destroyed BuildingSlam=PLACE2 ; placing building down sound RadarOn=COMMUP1 ; radar activation sound RadarOff=RADARDN1 ; radar deactivation sound ScoldSound=SCOLD8 ; generic scold sound TeslaCharge=OBELPOWR ; tesla charge up sound TeslaZap=OBELRAY1 ; tesla zap sound BlowupSound=EXPNEW01 ; sound when building is damaged to half strength ChuteSound=BOOP ; parachute deploy sound GenericClick=CLICKY1 ; generic click sound GenericBeep=BLEEP1 ; generic beep sound BuildingDrop=PLACE2 ; sound of building being placed down StopSound=Notify ;Sound when units are commanded to stop GuardSound=Notify ;Sound when units are commanded to guard ScatterSound=Notify ;Sound when units are commanded to scatter DeploySound=27-I002 ;Sound when units are commanded to deploy LightningSound= ; Commented out because sound was way too annoying (AI) TreeFire=FIRE2,FIRE1 ; small and large fires to attach to burning trees OnFire=FIRE3,FIRE2,FIRE1 ; list of flames to use when something catches fire [must be 3 in list] FlamingInfantry=FLAMEGUY ; anim to use for special onfire infantry logic Smoke=xxxx ; smoke that rises from the ground after a building explosion FirestormActiveAnim=GAFSDF_A FirestormIdleAnim=FSIDLE FirestormGroundAnim=FSGRND FirestormAirAnim=FSAIR MoveFlash=RING ; movement destination click feedback animation Parachute=PARACH ; big parachute used for paratroopers BombParachute=PARABOMB ; parachute used for parabombs and other parachuted ordinance SmallFire=FIRE3 ; animation for small fire [used after napalm] LargeFire=FIRE2 ; animation for large fire [used after napalm] AllyReveal=yes ; Allies automatically reveal radar maps to each other? ConditionRed=25% ; when damaged to this percentage, health bar turns red ConditionYellow=50% ; when damaged to this percentage, health bar turns yellow DropZoneRadius=4 ; distance around drop zone flair that map reveals itself DropZoneAnim=BEACON ; animation to use for the drop zone flair EnemyHealth=yes ; Show enemy health bar graph when selected? Gravity=6 ; gravity constant for ballistic projectiles IdleActionFrequency=.15 ; average minutes between infantry performing idle actions MessageDelay=.6 ; time duration of multiplayer messages displayed over map MovieTime=.06 ; minutes that movie recorder will record when activated (debug version only) NamedCivilians=no ; Show true names over civilians and civilian buildings? SavourDelay=.1 ; delay between scenario end and ending movie [keep the delay short] ShroudRate=4 ; minutes between each shroud creep process [0 means no shadow creep] FogRate=.5 IceGrowthRate=1.5 IceSolidifyFrameTime=1000 ; how many frames between when ice is cracked and when it gets solidified IceCrackSounds=ICECRAK1,ICECRAK2,ICECRAK3 AmbientChangeRate=.2 ; how many minutes between ambient light recalculations AmbientChangeStep=.1 ; step rate for gradually changing ambient lighting SpeakDelay=2 ; minutes between EVA repeating advice to the player TimerWarning=2 ; if mission timer is less than this many minutes, then display in red ExtraUnitLight=.2 ; Extra light to make units glow. ExtraInfantryLight=.2 ; Extra light to make infantry glow. ExtraAircraftLight=.2 ; Extra light to make aircraft glow. EMPulseSparkles=EMP_FX01 ; Anim to play over units disabled by an EM Pulse. WebbedInfantry=WEBGUY ; ******* Crate rules ******* ; General crate rules and controls are specified here. [CrateRules] CrateMaximum=255 ; crates can never exceed this quantity CrateMinimum=1 ; crates are normally one per human player but never below this number CrateRadius=3.0 ; radius (cells) for area effect crate powerup bonuses CrateRegen=3 ; average minutes between random powerup crate regeneration SilverCrate=HealBase ; solo play silver crate bonus SoloCrateMoney=2000 ; money to give for money crate in solo play missions UnitCrateType=none ; specifies specific unit type for unit type crate ['none' means pick randomly] WoodCrate=Money ; solo play wood crate bonus HealCrateSound=HEALER1 ; heal crate sound effect WoodCrateImg=CRATE ; wood crate overlay image to use CrateImg=CRATE ; normal crate overlay image to use FreeMCV=yes ; Give free MCV from crate if no buildings but still has money [multiplay only]? ; ******* Combat and damage rules ******* ; General rules that control combat, damage, or related items are listed here. [CombatDamage] AmmoCrateDamage=200 ; damage generated from exploding ammo crate overlay IonCannonDamage=751 HarvesterImmune=no ; Are harvester immune to normal combat damage? DestroyableBridges=yes ; Can bridges be destroyed? TiberiumExplosive=yes ; Is tiberium extra explosive? Scorches=BURN01,BURN02,BURN03,BURN04 ; scorch mark smudge types Scorches1=BURN05,BURN06,BURN07 ; scorch mark smudge types Scorches2=BURN08,BURN09,BURN10 ; scorch mark smudge types Scorches3=BURN11,BURN12,BURN13 ; scorch mark smudge types Scorches4=BURN14,BURN15,BURN16 ; scorch mark smudge types TiberiumExplosionDamage = 100 ; the amount of damage dealt out by explosion in a big tiberium chain reaction TiberiumStrength = 20 ; the higher this value, the harder it is to get big tiberium to explode Craters=CR1,CR2,CR3,CR4,CR5,CR6 ; crater smudge types AtomDamage=1000 ; damage points when nuclear bomb explodes (regardless of source) BallisticScatter=1.0 ; maximum scatter distance (cells) for inaccurate ballistic projectiles BridgeStrength=1500 ; strength of bridge [smaller means more easily destroyed] C4Delay=.03 ; minutes to delay after placing C4 before building will explode C4Warhead=HE ; this is the warhead that C4 uses to damage buildings FirestormWarhead=FirestormWH ; the warhead that the firestorm defense uses when active IonCannonWarhead=IonCannonWH ; the warhead that the ion cannon uses VeinholeWarhead=VeinholeWH ;particle system defaults DefaultFirestormExplosionSystem=FirestormSparkSys ; the particle system to use when the firestorm defense blows something up DefaultLargeGreySmokeSystem=BigGreySmokeSys DefaultSmallGreySmokeSystem=SmallGreySSys DefaultSparkSystem=SparkSys DefaultLargeRedSmokeSystem=BigGreySmokeSys DefaultSmallRedSmokeSystem=SmallGreySSys DefaultDebrisSmokeSystem=SmallGreySSys DefaultFireStreamSystem=FireStreamSys DefaultTestParticleSystem=TestSmokeSys DefaultRepairParticleSystem=WeldingSys Crush=1.8 ; if this close (cells) to crushable target, then crush instead of firing upon it (computer only) ExpSpread=.7 ; cell damage spread per 100 damage points for exploding object types [if Explodes=yes] FireSupress=1 ; radius from target to look for friendlies and thus discourage firing upon, if found FlameDamage=Fire ; damage (warhead type) to use when on object is in flames FlameDamage2=Fire2 HomingScatter=2.0 ; maximum scatter distance (cells) for inaccurate homing projectiles MaxDamage=1000 ; maximum damage (after adjustments) per shot MinDamage=1 ; minimum damage (after adjustments) per shot PlayerAutoCrush=no ; Will player controlled units automatically try to crush enemy infantry? PlayerReturnFire=no ; More aggressive return fire from player controlled objects? PlayerScatter=no ; Will player units scatter, of their own accord, from threats and damage? ;ProneDamage=50% ; when infantry is prone, damage is reduced to this percentage SplashList=H2O_EXP3,H2O_EXP2,H2O_EXP1 ; water explosion set for conventional explosives TreeTargeting=no ; Automatically show target cursor when over trees? TurboBoost=1.5 ; speed multiplier for turbo-boosted weapons when firing upon aircraft Incoming=10 ; If an incoming projectile is as slow or slower than this, then ; object in the target location will try to run away. ; Grenades have this characteristic. CollapseChance=100 ; Percent chance that a cliff will collapse when hit. BerzerkAllowed=no ; Allow Cyborgs to go berzerk when at half damage? ; *** Animation List *** ; This is the complete list of animations available. There are ; internal tables that rely on this exact order. Additional ; animations should be appended to the end. [Animations] 1=TWLT100 3=ELECTRO ; The following can occur in any order. 240=TWLT026 241=TWLT036 242=TWLT050 243=TWLT070 244=TWLT100 245=TWLT070T 246=TWLT100I 250=S_BANG16 251=S_BANG24 252=S_BANG34 253=S_BANG48 260=S_BRNL20 261=S_BRNL30 262=S_BRNL40 263=S_BRNL58 270=S_CLSN16 271=S_CLSN22 272=S_CLSN30 273=S_CLSN42 274=S_CLSN58 280=S_TUMU22 281=S_TUMU30 282=S_TUMU42 283=S_TUMU60 290=RING1 291=IONBEAM 12=SMOKEY 13=BURN-S 14=BURN-M 15=BURN-L 22=H2O_EXP1 23=H2O_EXP2 24=H2O_EXP3 25=PARACH 26=PARABOMB 28=RING 30=PIFF 31=PIFFPIFF 32=FIRE3 33=FIRE2 34=FIRE1 35=FIRE4 42=GUNFIRE 43=TWINKLE1 44=TWINKLE2 45=TWINKLE3 47=MONEY 48=MLTIMISL 49=HEALONE 50=HEALALL 51=ARMOR 52=CHEMISLE 53=CLOAK 54=FIREPOWR 63=MGUN-N 64=MGUN-NE 65=MGUN-E 66=MGUN-SE 67=MGUN-S 68=MGUN-SW 69=MGUN-W 70=MGUN-NW 71=SMOKLAND 72=VETERAN 73=REVEAL 74=SHROUDX 82=GAPOWR_A 83=GAPOWR_AD 84=NARADR_A 85=NARADR_AD 90=GAWEAP_1 91=GAWEAP_2 92=GAWEAP_A 93=GAWEAP_B 94=GAWEAP_C 95=GAWEAP_D 96=GAPILE_A 97=GAPILE_B 98=NAPULS_A 99=GACTWR_A 100=GACTWR_B 101=GACTWR_C 102=GACTWR_D 103=GAPILE_C 104=GAWEAP_1 105=GAWEAP_2 106=GAWEAP_A 107=GAWEAP_B ;108=GACOMM_A ;109=GACOMM_B ;110=GACOMM_C ;111=GACOMM_D ;112=GACOMM_AD 113=NASTLH_A 114=NASTLH_AD 115=GACNSTMK 116=GACNST_A 117=GACNST_AD 118=GACNST_B 119=GACNST_C 120=GACNST_CD 121=GACNST_D 122=NAHAND_A 123=NAHAND_B 124=NAHAND_BD 125=GAPILE_CD 126=NATMPL_A 127=NATMPLMK 128=NAREFN_A 129=NAREFN_B 130=NAREFN_C 131=GAHPAD_A 132=GAHPAD_AD 133=GAPOWR_B 134=GADEPT_A 135=GADEPT_AD 136=GADEPT_B 137=GATECH_A 138=GATECH_AD 139=NATECH_A 143=NAWAST_A 144=NAWAST_AD 145=NAWAST_B 146=NAWAST_BD 147=NAOBEL_A 148=NAMISL_A 149=NAMISL_AD 150=NAMISL_B 151=NAMISL_BD 152=GAFIRE_A 153=GAFIRE_B 154=GAFIRE_C 155=NAREFN_AR 156=NAPOST_A 157=NAPOST_AD 158=NAPOST_B 159=WA01X 160=WA02X 161=WA03X 162=WA04X 163=WB01X 164=WB02X 165=WB03X 166=WB04X 167=WC01X 168=WC02X 169=WC03X 170=WC04X 171=WD01X 172=WD02X 173=WD03X 174=WD04X 175=TREESPRD 176=NAOBEL_B 177=GADEPT_C1 178=GADEPT_C2 179=GADEPT_C3 180=GADEPT_D 181=GADEPT_DD 182=GASILO_A 183=GASILO_AD 184=GASILO_B 185=GASILO_BD 186=NAPOWR_A 187=NAPOWR_AD 188=CAHOSP_A 189=NAAPWR_A 190=NAAPWR_AD 191=GASPOT_A 192=GASPOT_AD 193=CTDAM_A 194=CTDAM_AD 195=TUNTOP01 196=TUNTOP02 197=TUNTOP03 198=TUNTOP04 199=NTPYRA_A 200=NTPYRA_AD 201=PULSEFX1 202=GADPSAMK 203=METLARGE 204=METSMALL 205=METDEBRI 206=METSTRAL 207=METLTRAL 208=PULSBALL 209=GAFSDF_A 210=FSIDLE 211=FSAIR 212=FSGRND 213=CAARMR_A 214=GADPSA_A 215=GATICK_A 216=GATICKMK ;217=UFO 218=CAARAY_A 219=CAARAY_B 220=CAARAY_C 221=CAARAY_CD 222=CAARAY_D 223=CAARAY_DD 224=GAICBM_A 225=GAICBMMK 226=NAHPAD_A 227=NAHPAD_AD 228=GAKODK_A 229=GAKODK_AD 230=GAKODK_B 231=GAKODK_C 232=GAKODK_CD 233=NAMNTK_A 234=CTDAM_B 235=CTDAM_BD 236=CARYLAND 237=DROPLAND 300=GAPLUG_A 301=GAPLUG_B 302=GAPLUG_BD 303=GAPLUG_C 304=GAPLUG_D 305=GAPLUG_E 306=GAPLUG_F 307=GARADR_A 308=GARADR_AD 309=NASAM_A 310=EMP_FX01 320=DIG 400=VEINATAC 500=INFDIE 501=DIRTEXPL 502=PULSEFX2 510=DBRIS1LG 511=DBRIS1SM 512=DBRIS2LG 513=DBRIS2SM 514=DBRIS3LG 515=DBRIS3SM 516=DBRIS4LG 517=DBRIS4SM 518=DBRIS5LG 519=DBRIS5SM 520=DBRIS6LG 521=DBRIS6SM 522=DBRIS7LG 523=DBRIS7SM 524=DBRIS8LG 525=DBRIS8SM 526=DBRIS9LG 527=DBRIS9SM 528=DBRS10LG 529=DBRS10SM 550=DEATH_A 551=DEATH_B 552=DEATH_C 553=DEATH_D 554=DEATH_E 555=DEATH_F 556=DROPPOD 557=DROPPOD2 558=FLAMEGUY 600=EXPLOSML 601=EXPLOMED 602=EXPLOLRG 603=XGRYMED1 604=XGRYMED2 605=XGRYSML1 606=XGRYSML2 610=STEAMPUF 611=SMOKEY2 612=PULSE 613=WAKE1 614=WAKE2 618=BEACON 619=PODRING 620=CLDRNGL1 621=CLDRNGL2 622=CLDRNGMD 623=CLDRNGSM 700=CRYSTAL1 701=CRYSTAL2 702=CRYSTAL3 703=CRYSTAL4 704=BIGBLUE 705=SGRYSMK1 706=DROPEXP 707=INVISO 708=WEBGUY 709=WEB 710=K_LIGHT1 711=K_LIGHT2 712=MWAR_1 713=MWAR_2 714=MWAR_A 715=MWAR_B 716=MWAR_C 717=MWAR_D 718=MWARMK 719=DLIMP_A 720=DJUGG 721=DJUGG_A 722=DJUGGMK 723=MSTLMK 724=MSTL_A 725=DEFDMK 726=CORE_A 727=CORE_AD 728=CORE_B 729=CORE_BD 730=CORE_C 731=CORE_CD 732=OBL1_A 733=OBL1_AD 734=OBL1_B 735=OBL1_BD 736=OBL1_C 737=OBL1_CD 738=OBL2_A 739=OBL2_AD 740=OBL2_B 741=OBL2_BD 742=OBL2_C 743=OBL2_CD 744=DEFD_EXP 745=MEMPFX 746=NAWEAP_A 747=NAWEAP_AD 748=NATECH_AD 749=BIGBLUE3 ; ******* Multiplayer Settings ******* ; These are the multiplayer dialog default settings. Does not apply to ; Westwood chat, only to the in-game dialogs. [MultiplayerDefaults] Money=10000 MaxMoney=10000 ShadowGrow=no Bases=yes TiberiumGrows=yes Crates=yes CaptureTheFlag=no ; ******* Special weapon charge times ******* ; The time (minutes) for recharge of these special weapons. ;[Recharge] ;Nuke=13 ; nuclear missile ;EMPulse=5 ; nuclear missile ;IonCannon=11 ;FirestormDefense=4 ; ******* Object Heap Maximums ******* ; These are the absolute maximum number of these object types ; allowed in the game (at any one time). [Maximums] Players=8 ; ipx layer limits this to 8 maximum ; ******* AI Controls ******* ; Computer Skirmish-Mode behavior controls. The ratio values are based on the ; number of buildings in the computer base that should be of the type specified. ; The ratio total should exceed 100% so that the base will always try to grow as ; it vainly attempts to achieve the specified percentage composition. ; These AI controls are held over from Red Alert. They will be replaced or augmented ; by Tiberian Sun improved AI subsystems. Changing these values will be only ; temporary until the new system comes on line. [AI] BuildConst=GACNST BuildPower=NAPOWR,GAPOWR,NAAPWR ; buildings to build to generate power BuildRefinery=PROC ; refinery ratio based on these buildings BuildBarracks=NAHAND,GAPILE ; barracks ratio based on these buildings BuildTech=NATECH,GATECH ; should build on each of these BuildWeapons=GAWEAP,NAWEAP,DGWEAP,DNWEAP ; war factory ration based on these buildings BuildDefense=NAOBEL ; base defenses are based on these buildings BuildPDefense=NAOBEL ; excess power base defense BuildAA=NASAM ; air defenses based on these buildings BuildHelipad=GAHPAD,NAHPAD ; air helicopter offense based on these buildings BuildRadar=GARADR,NARADR ConcreteWalls=GAWALL,NAWALL NSGates=NAGATE_B,GAGATE_B EWGates=NAGATE_A,GAGATE_A GDIWallDefense=6 GDIWallDefenseCoefficient=3 NodBaseDefenseCoefficient=1.2 GDIBaseDefenseCoefficient=1.5 MaximumBaseDefenseValue=60 ComputerBaseDefenseResponse=3 ; how much does the computer overrespond to attacks on its base? AttackInterval=3 ; average delay between computer attacks AttackDelay=5 ; average delay time before computer begins first attack PatrolScan=.016 ; minute interval between scanning for enemys while patrolling. CreditReserve=100 ; Structure repair will not begin if available cash falls below this amount. PathDelay=.01 ; Delay (minutes) between retrying when path is blocked. BlockagePathDelay=60 ; delay (frames) before unit paths around all blockage TiberiumNearScan=6 ; cell radius to scan when harvesting a single patch of Tiberium TiberiumFarScan=48 ; cells radius to scan when looking for a new Tiberium patch to harvest AutocreateTime=5 ; average minutes between creating an 'autocreate' team InfantryReserve=3000 ; always build infantry if cash reserve is greater than this InfantryBaseMult=1 ; build infantry if building count times this number is less than current infantry quantity PowerSurplus=50 ; build power plants until power surplus is at least this amount BaseSizeAdd=3 ; computer base size can be no larger than the largest human opponent, plus this quantity RefineryRatio=.16 ; ratio of base that should be composed of refineries RefineryLimit=4 ; never build more than this many refineries BarracksRatio=.16 ; ratio of base that should be composed of barracks BarracksLimit=2 ; never build more than this many barracks WarRatio=.1 ; ratio of base that should be composed of war factories WarLimit=2 ; never build more than this many war factories DefenseRatio=.4 ; ratio of base that should be defensive structures DefenseLimit=40 ; maximum number of defensive buildings to build AARatio=.14 ; ratio of base that should be anti-aircraft defense AALimit=10 ; maximum number of anti-aircraft buildings to build TeslaRatio=.16 ; ratio of base that should be telsa coils TeslaLimit=10 ; maximum number of tesla coils to build HelipadRatio=.12 ; ratio of base that should be composed of helipads HelipadLimit=5 ; maximum number of helipads to build AirstripRatio=.12 ; ratio of base that should be composed of airstrips AirstripLimit=5 ; maximum number of airstrips to build CompEasyBonus=no ; When more than one human in game, computer player goes to "easy" mode? Paranoid=yes ; Will computer players ally with each other if the situation looks bleak? PowerEmergency=75% ; sell buildings to raise power level if it falls below this percentage AIBaseSpacing=1 ; spacing between buildings when AI is building a base ; ******* Lists the AI general COM objects ******* ; These are COM objects that support the IAIHouse interface. [AIGenerals] ;1={F706E6E0-86DA-11D1-B706-00A024DDAFD1} ;2={9E0F6120-87C1-11D1-B707-00A024DDAFD1} ;3={C6004D80-87D1-11d1-B707-00A024DDAFD1} ;4={FBE6D4A0-87D1-11d1-B707-00A024DDAFD1} ;5={FBE6D4A1-87D1-11d1-B707-00A024DDAFD1} ; ******* IQ setting for computer activity ******* ; Each player (computer controlled or otherwise) is given an IQ rating that is used ; to control what the computer is allowed to automatically control. This is ; distinct from the difficulty setting. The higher the IQ setting, the more autonomous ; and intelligent the side will behave. Each ability is given a rating that ; indicates the IQ level (or above) that the ability will be granted. Because such ; abilities are automatically performed by the computer, giving a human controlled ; country a high IQ is not recommended. Otherwise the player's units will start to ; automatically "do their own thing"! A human controlled country is presumed to have ; an IQ rating of zero. A computer controlled country has an IQ of 1 or higher. ; When in skirmish mode or when multiplayer AIs are active, the computer IQ is set to ; the maximum. [IQ] MaxIQLevels=5 ; the maximum number of discrete IQ levels SuperWeapons=4 ; super weapons are automatically fired by computer Production=5 ; building/unit production is automatically controlled by computer GuardArea=2 ; newly produced units start in guard area mode RepairSell=1 ; allowed to choose repair or sell of damaged buildings AutoCrush=2 ; automatically try to crush antogonists if possible Scatter=2 ; will scatter from incoming threats [grenades and such] ContentScan=3 ; will consider contents of transport when picking good target Aircraft=3 ; automatically replace aircraft Harvester=2 ; automatically replace lost harvesters SellBack=2 ; allowed to sell buildings ; ******* Side Type List ******* ; The combantants can be grouped according to side. This ; lists the sides and their respective member houses. [Sides] GDI=GDI Nod=Nod Civilian=Neutral Mutant=Special ; *** House (players) List *** ; Each side has some basic controls on how they behave (when ; controlled by the computer. Here is the list of available ; house types. [Houses] 00=GDI 01=Nod 02=Neutral 03=Special ;This section only affects FinalSun and FinalSun won't be able to start if this section does not exist. ;The number of houses listed here needs to match that of the [Houses] section in FSR.ini to allow all Spawn houses to be displayed. ;By default FinalSun only displays the houses of the [Houses] section from FSR.ini and ;the houses of this [SPHouses] section are only used when you click "Standard houses" in FinalSun [SPHouses] 00=GDI 01=Nod 02=Neutral 03=Special 04=Extra1 05=Extra2 06=Extra3 07=Extra4 08=Extra5 09=Extra6 10=Extra7 11=Extra8 ; ******* Country Statistics ******* ; Certain countries have special adjustments to their unit and building ; values. These are global values that affect ALL units and buildings owned ; by that country. This applies only to multiplayer games and skirmish mode. In ; normal game play, all values are "1.0". ; Airspeed = multiplier to speed for all air units [larger means faster] (def=1.0) ; Armor = multiplier to armor strength for all units and buildings [larger means stronger] (def=1.0) ; Cost = multiplier to cost for all units and buildings [larger means costlier] (def=1.0) ; Firepower = multiplier to firepower for all weapons [larger means more damage] (def=1.0) ; Groundspeed = multiplier to speed for all ground units [larger means faster] (def=1.0) ; ROF = multiplier to Rate Of Fire for all weapons [larger means slower ROF] (def=1.0) ; BuildTime = multiplier to general object build time [larger means longer to build] (def=1.0) ; Color = color to use when displaying objects owned by this country [see color schemes] ; Multiplay = This house used as placeholder for multiplay house (def=no)? ; WallOwner = Will this house own walls that are placed near its buildings (def=yes)? ; SmartAI = Does it presume to have the smart AI logic already enabled (def=no)? [GDI] Name=GDI Suffix=GDI Prefix=G Color=Gold Multiplay=yes Side=GDI [Nod] Name=NOD Suffix=NOD Prefix=N Color=DarkRed Multiplay=yes Side=Nod SmartAI=yes [Special] Name=JP Suffix=JP Prefix=J Color=Grey Side=Mutant SmartAI=yes MultiplayPassive=true [Neutral] Name=Civilian Suffix=CIV Prefix=C Color=Grey MultiplayPassive=true SmartAI=yes Side=Civilian ; ******* Color Schemes ******* ; Each country must be assigned a color. This lists the various ; colors available. The values represent the 'hue', 'saturation', ; and 'value'. The 'value' component specifies the maximum brightness ; allowed for the color as the color spread is generated. The 'hue' ; component remains constant. The 'saturation' curves through color ; space as the 'value' component changes such that darker colors ; become more saturated. [Colors] ; Col. Mustard LightGold=34,128,255 ; 0 - TopBar - Options, Credit Gold=34,160,255 ; 1 DarkGold=34,235,255 ; 2 ; Mrs White LightGrey=0,0,220 ; 3 - Civilians, CameoText, QueueCount Grey=0,0,190 ; 4 DarkGrey=0,0,120 ; 5 Black=0,100,0 ; 6 ;as white as we can get White=0,0,255 ; 7 ; Miss Scarlet LightRed=0,70,255 ; 8 Red=0,160,255 ; 9 DarkRed=0,235,255 ; 10 Burgandy=0,255,150 ; 11 ;Orange Julius LightOrange=24,165,255 ; 12 Orange=24,255,255 ; 13 DarkOrange=11,235,255 ; 14 ; Mrs Peacock LightMagenta=228,120,255 ; 15 Magenta=228,160,255 ; 16 DarkMagenta=228,235,255 ; 17 ; Prof. Plum LightPurple=200,160,255 ; 18 Purple=200,235,255 ; 19 HyundaiPurple=200,235,170 ; 20 ;Little Boy Blue LightBlue=164,140,255 ; 21 - CameoText Ready for Buildings/Superweapon Blue=164,200,255 ; 22 DarkBlue=164,200,179 ; 23 NeonBlue=164,255,255 ; 24 ;Sky LightSky=142,70,255 ; 25 Sky=142,160,255 ; 26 DarkSky=142,235,255 ; 27 ;Cyan LightCyan=132,70,255 ; 28 Cyan=132,160,255 ; 29 DarkCyan=132,235,255 ; 30 ;Teal LightTeal=110,70,255 ; 31 Teal=110,160,255 ; 32 DarkTeal=110,235,255 ; 33 ; Mr. Green LightGreen=85,70,255 ; 34 Green=85,160,200 ; 35 DarkGreen=85,235,150 ; 36 NeonGreen=85,255,255 ; 37 ;Mellow Yellow LightYellow=43,70,255 ; 38 Yellow=43,160,255 ; 39 DarkYellow=43,235,255 ; 40 NeonYellow=43,255,255 ; 41 ;Life is Peachy LightPeach=21,120,255 ; 42 Peach=21,150,255 ; 43 DarkPeach=21,180,255 ; 44 DarkerPeach=21,255,255 ; 45 ;Lemon lime LightLime=53,70,255 ; 46 Lime=53,160,255 ; 47 Darklime=53,235,200 ; 48 NeonLime=53,235,255 ; 49 ; ******* Difficulty Settings ******* ; Game difficulty is controlled by these factors. Some of these factors will ; only affect a computer player. The computer and the player are handicapped by ; individual settings. Thus the computer may be playing at 'difficult' level while the ; player may be playing at 'easy' level. ; Airspeed = multiplier to speed for all air units (def=1.0) ; Armor = multiplier to armor strength for all units and buildings (def=1.0) ; Cost = multiplier to cost for all units and buildings (def=1.0) ; Firepower = multiplier to firepower for all weapons (def=1.0) ; Groundspeed = multiplier to speed for all ground units (def=1.0) ; ROF = multiplier to Rate Of Fire for all weapons [larger means slower ROF] (def=1.0) ; BuildSlowdown = Should the computer build slower than the player (def=no)? ; <<< affects the computer player, not the human player >>> ; ContentScan = Should the contents of a transport be considered when picking best target (def=no)? ; RepairDelay = average delay (minutes) between initiating building repair ; BuildDelay = average delay (minutes) between initiating construction ; DestroyWalls = Allow scanning for nearby enemy walls and destroy them (def=yes)? [Easy] Groundspeed=1.0 Airspeed=1.0 BuildTime=.8 Armor=1.2 ROF=.8 Cost=1.0 RepairDelay=.02 BuildDelay=.03 DestroyWalls=yes ContentScan=yes [Normal] Groundspeed=1.0 Airspeed=1.0 BuildTime=1 Armor=1.0 ROF=1.0 Cost=1.0 RepairDelay=.02 BuildDelay=.03 BuildSlowdown=yes DestroyWalls=yes ContentScan=yes [Difficult] Groundspeed=1.0 Airspeed=1.0 BuildTime=1.0 Armor=.8 ROF=1.2 Cost=1.0 RepairDelay=.05 BuildDelay=.1 BuildSlowdown=yes DestroyWalls=no ; ******* Unit Statistics ******* ; Specifies the characteristics of the various game objects. ; AllowedToStartInMultiplayer = Can the unit be allocated to a player when starting a multiplayer game (def=yes) ; Ammo = number of rounds carried between reloads [-1 means unlimited] (def=-1) ; Armor = the armor type of this object [none,wood,light,heavy,concrete] (def=none) ; BuildLimit = arbitrary maximum allowed to build [per house] (def=-1 -- no restriction) ; Cloakable = Is it equipped with a cloaking device (def=no)? ; Cost = cost to build object (in credits) ; Category = category of object [used by AI systems -- "Soldier", "Civilian", "VIP", "Ship", ; "Recon", "AFV", "IFV", "LRFS", "Support", "Transport", "AirPower", "AirLift"] ; CloakStop = Does the unit cloak when stopped moving (def=no)? ; Crewed = Does it contain a crew that can escape [never infantry] (def=no)? ; CrushSound = sound to play if this object type is crushed (def=none) ; DeployTime = time, in minutes, to deploy or undeploy [if this object can do so] ; Disableable = Can this object be disabled by special multiplay option (def=yes)? ; DoubleOwned = Can be built/owned by all countries in a multiplayer game (def=no)? ; Explodes = Does it explode violently when destroyed [i.e., does it do collateral damage] (def=no)? ; Explosion = the explosion to use when it blows up [doesn't apply to infantry] (def=none) ; FireAngle = pitch of projectile launch [64 = horizontal, 0 = vertical] (def=50) ; Gate = Is this building a gate? (def=no) ; GateCloseDelay = time, in minutes, to delay before closing a gate after it has opened. ; GuardRange = distance to scan for enemies to attack (def=use weapon range) ; Image = name of graphic data to use for this object (def=same as object identifier) ; Immune = Is this object immune to damage ; ImmuneToVeins = Is it immune to vein creature attacks (def=no)? ; Invisible = Is completely and always invisible to enemy (def=no)? ; Insignificant = Will this object not be announed when destroyed (def=no)? ; LegalTarget = Is this allowed to be a combat target (def=yes)? ; Name = specifies the given name (displayed) for the object ; Nominal = Always use the given name rather than generic "enemy object" (def=no)? ; Owner = who can build this [GDI or Nod] (def=none) ; PipScale = what to base pip display on [Passengers, Tiberium, Ammo, Power] (def=none) ; Points = point value for scoring purposes (def=0) ; Prerequisite = list of buildings needed before this can be manufactured (def=no requirement) ; Primary = primary weapon equipped with (def=none) ; Secondary = secondary weapon equipped with (def=none) ; Elite = new primary weapon when at elite veteran status (def=same as primary) ; RadarVisible = Is visible on radar even when under shroud (def=yes [infantry=no])? ; ROT = Rate Of Turn for body (if present) and turret (if present) (def=0) ; Reload = time delay between reloads (def=0) ; RadarInvisible = Is it invisible on radar maps (def=no)? ; SelfHealing = Does the object heal automatically up to half strength (def=no)? ; Selectable = Can this object be selected by the player (def=yes)? ; Sensors = Has sensors to detect nearby cloaked objects (def=no)? ; Sight = sight range, in cells (def=1) ; Storage = the number of 'bails' this building or unit can store (def=0) ; Strength = strength (hit points) of this object ; TargetLaser = Does it have a targeting laser (def=no)? ; Trainable = Can this object become veteran by experience (def=yes, buildings def=no)? ; Turret = Is it equipped with a turret like superstructure [never infantry] (def=no)? ; TurretSpins = Does the turret just sit and spin [only if turret equipped] (def=no)? ; TechLevel = tech level required to build this [-1 means can't build] (def=-1) ; ToProtect = Should friendly units come to rescue if under attack [computer only] (def=no)? ; TypeImmune = Immune to damage from same type objects if owned by same side? ; VoiceSelect = list of voices when selecting this object (def=none) ; VoiceMove = list of voices to use when giving object a movement order (def=none) ; VoiceAttack = list of voices to use when giving object an attack order (def=none) ; VoiceDie = list of voices to use when it dies (def=none) ; VoiceFeedback = list of voices that may give when taking damage (def=none) ; Locomotor = CLSID of the object handling movement for this object (def=statue) ; VeteranAbilities = list of veteran abilities to grant (def=none) ; EliteAbilities = list of elite abilities to grant cumulative with veteran abilities (def=none) ; [FASTER,STRONGER,FIREPOWER,SCATTER,ROF,SIGHT, ; CLOAK,TIBERIUM_PROOF,VEIN_PROOF,SELF_HEAL,EXPLODES, ; RADAR_INVISIBLE,SENSORS,FEARLESS,C4,TIBERIUM_HEAL, ; GUARD_AREA,CRUSHER] ; NonVehicle = Are repair units unable to repair this unit? ; TooBigForCarryalls = Should Carryalls be unable to airlift this (def=no)? ; IsVehicleTransport = Is this able to carry vehicles as passengers (def=no)? ; <<< applies only to infantry types >>> ; Agent = Does it have spy-like abilities (def=no)? ; Fearless = Is not prone to fear (def=no)? ; VoiceComment = list of idle voices (def=none) ; Pip = color of pip when inside a transport [green,yellow,white,red,blue] (def=green) ; C4 = Equipped with building sabotage explosives [presumes Infiltrate is true] (def=no)? ; Cyborg = Does it require special cyborg death handling (def=no)? ; Fraidycat = Is it inherently afraid and will panic easily (def=no)? ; TiberiumProof = Is it immune to tiberium and tiberium gas damage (def=no)? ; Infiltrate = Can it enter a building like a spy or thief (def=no)? ; IsCanine = Should special case dog logic be applied to this? ; Civilian = Counts a civilian for evac and kill tracking (def=no)? ; FemaleVoice = Uses the civilian female voice (def=no)? ; Engineer = Does it behave like an engineer as far as repair and capture go (def=no)? ; Disguised = Is it disguised as enemy soldier when seen by enemy (def=no)? ; Agent = Does this infantry gather information if it enters an enemy building [like a spy] (def=no)? ; Mechanic = Can this infantry repair vehicles (def=no)? ; VehicleThief = Does it steal enemy vehicles when it gets close to one (def=no)? ; <<< applies only to moving units (not buildings) >>> ; MoveToShroud = Allowed to move into a shrouded cell (def=yes, aircraft def=no)? ; Dock = preferred docking building [e.g., harvester -> refinery, helicopter -> helipad] (def=none) ; TiberiumHeal = Does it heal slowly when in Tiberium field (def=no)? ; Passengers = number of passengers it may carry (def=0) ; Speed = speed of this object [n/a for buildings] (def=0) ; ManualReload = Must this object reload by coordinating with reloader building (def=no)? ; WalkRate = walking animation rate [larger means slower] (def=1) ; <<< applies only to terrestrial driving vehicle types >>> ; CrateGoodie = Can it appear out of a crate in multiplay (def=no)? ; Crushable = Can it be crushed by a heavy tracked vehicle (def=no)? ; Crusher = Is this vehicle able to crush infantry (def=no)? ; NoMovingFire = The vehicle must stop before it can fire (def=no)? ; DeployToFire = The vehicle must deploy before it can fire (def=no)? ; Harvester = Does the special Tiberium harvesting rules apply (def=no)? ; Weeder = Does the special weed-harvesting rules apply (def=no)? ; Deployer = Does it deploy before being able to operate (def=no)? OBSOLETE ; IsTilter = Does this unit tilt on slopes (def=yes)? ; CarriesCrate = Might this unit drop a crate when it is destroyed (def=no)? ; <<< applies only to aircraft >>> ; Carryall = Can it tote vehicles around (def=no)? ; Landable = Can this aircraft land on the map (def=no)? ; PitchSpeed = Throttle setting at which aircraft pitch forward (def=.25); ; PitchAngle = Amount that non-FixedWing aircraft pitch forward in degrees (def=20.0); ; RollAngle = Amount that the aircraft rolls when turning (def=30.0) ; <<< applies only to building types >>> ; Adjacent = distance allowed to place from other buildings (def=1) ; BaseNormal = Considered for building adjacency checks (def=yes)? ; Barrel = Use barrel explosion logic when it is destroyed (def=no)? ; Bib = Does the building have a bib built in (def=no)? ; Capturable = Can this building be infiltrated by a spy/engineer (def=no)? ; DockUnload = When a unit docks with this building should it unload (def=no)? ; Factory = type of object to build [InfantryType, AircraftType, UnitType, BuildingType, VesselType] (def=none) ; Fake = Is this a fake structure (def=no)? ; FreeUnit = free unit to give this building [typically harvester with refinery] (def=none) ; Power = power output [positive for output, negative for drain] (def=0) ; Powered = Does it require power to function (def=no)? ; Radar = Does this building give radar to owning player (def=no)? ; Repairable = Can it be repaired (def=yes)? ; UnitReload = Does this building reload units if they dock with it (def=no)? ; UnitRepair = Does this building repair units if they dock with it (def=no)? ; Unsellable = Cannot sell this building (even if it can be built)? ; Wall = Is this a wall type structure [special rules apply] (def=no)? ; WaterBound = Is this building placed on water only (def=no)? ; Upgrades = Is the number of power-ups/upgrades that can be applied to this building (def=0) ; ShipYard = This building is a ship yard or sub pen ; SAM = This building is a SAM launcher ; ConstructionYard = This building is a construction yard ; Refinery = This building is a tiberium/ore refinery ; WeaponsFactory = This building is a weapons factory ; CloakGenerator = Does this building cloak objects around it? ; LaserFencePost = This building is a laser fence post and obeys the rules for a building of this type. ; LightIntensity = This building radiates this amount of light (def = 0). ; LightVisibility= The distance (in leptons) that this light is visible from (def=5000). ; LightRedTint = The red tint of this buildings light (def=1.0) ; LightGreenTint = The green tint of this buildings light (def=1.0) ; LightBlueTint = The blue tint of this buildings light (def=1.0) ; InvisibleInGame= Building cannot be seen on selected in the game, only in the editor. (def=no) ; PowersUpBuilding = Building that can be upgraded by attaching this building to it ; PowersUpToLevel = Amount of upgrade provided by this attachment. -1=incremental upgrade. Positive number is specific upgrade. ; Hospital = Can this building heal infantry (def = no) ? ; Armory = Is this building an armory ; PlaceAnywhere = Can this building ignore normal placement rules? Only use this for non-player placed buildings (def = no). ; Weeder = Is this a weed collection facility (def=no)? ; TogglePower = [override] Can be turned on/off under player control or affected by low power (def=yes)? ; TurretChargeAnimRate = The rate at which this building charges before firing a weapon (higher is slower, def = 3). ; ProduceCashAmount = Gives the specified amount of credits to the owner of the structure every 180 frames (def = 0, max = 255). ; ; WST 6/23/99. Below are new zbuffer adjustment for units ; ZFudgeCliff // fudge for units behind cliffs showing through rocks ; ZFudgeColumn // fudge for units behind bridge overpass support columns ; ZFudgeTunnel // fudge for unit behind tunnel entrances ; ZFudgeBridge // fudge for tall units when they are under a bridge... eg mammoth mk2 ; ; ******* Vehicle Types ******* ; This lists all of the vehicles types in the game. Each vehicle ; type should have a matching section that specifies the data it ; requires. [VehicleTypes] 00=HARV ;index 0 is used in fsgdi07.map 01=HORV 02=APC 03=BIKE 04=MMCH 05=TTNK 06=REPAIR 07=SMECH 08=BGGY 09=FLMTNK 10=HVR 11=ART2 12=LPST 13=JUGG 14=LIMPET 15=MOBILEMP 16=CMOBILEMP 17=MCV 18=SAPC 19=STNK 20=SUBTANK 21=SONIC 22=REAPER 23=4TNK 24=HMEC 25=MOBWARG 26=MOBWARN 27=SGEN 28=WEED 29=BASEUNIT 30=GHUNTER 31=NHUNTER 32=DEFENDER 33=CAR 34=PICK 35=WINI 36=BUS 37=TRUCKA 38=TRUCKB 39=ICBM 40=LOCOMOTIVE 41=TRAINCAR 42=CARGOCAR 43=VISC_SML 44=VISC_LRG 45=JFISH ; Hover MLRS (hover multi-launch rocket system) [HVR] Name=Hover MLRS Category=AFV TargetLaser=yes FireAngle=32 Prerequisite=GDIFACTORY,GARADR Primary=HoverMissile TooBigToFitUnderBridge=true Strength=230 Armor=wood TechLevel=7 CrateGoodie=yes Sight=7 Speed=7 Owner=GDI Cost=900 Turret=yes Points=30 ROT=5 Crusher=no Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 SpeedType=Hover Locomotor={4A582742-9839-11d1-B709-00A024DDAFD1} MovementZone=AmphibiousDestroyer ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys EliteAbilities=SELF_HEAL ZFudgeColumn=12 ZFudgeTunnel=15 ; Mammoth tank [4TNK] Name=Mammoth Tank Category=AFV TargetLaser=yes Primary=120mmx Secondary=MammothTusk Strength=600 CrateGoodie=yes Armor=heavy Turret=yes TechLevel=-1 Sight=6 Speed=4 Owner=GDI Cost=1700 Points=60 ROT=5 Crusher=yes SelfHealing=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Destroyer ThreatPosed=40 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys AllowedToStartInMultiplayer=no ZFudgeColumn=9 ZFudgeTunnel=15 [TRUCKA] Name=Truck Category=AFV Primary=none Secondary=none Strength=200 Armor=light Turret=no TechLevel=-1 Sight=5 Speed=4 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=500 Points=40 ROT=5 Crusher=no SelfHealing=no Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=2 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys MaxDebris=2 DebrisTypes=TIRE DebrisMaximums=4 [TRUCKB] Name=Truck (loaded) Category=AFV Primary=none Secondary=none Strength=200 Armor=light Turret=no TechLevel=-1 Sight=5 Speed=4 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=500 Points=40 ROT=5 Crusher=no SelfHealing=no Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=2 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=2 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys DebrisTypes=TIRE DebrisMaximums=4 CarriesCrate=yes ; Deployable Sensor Array [LPST] Name=Mobile Sensor Array Category=Support Prerequisite=FACTORY,RADAR Strength=600 ;CloakRadiusInCells=20 RadarInvisible=yes Armor=wood TechLevel=6 Sight=10 Speed=6 Owner=GDI,Nod AllowedToStartInMultiplayer=no Turret=no Cost=950 Points=30 ROT=5 DeploysInto=GADPSA Crusher=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=3 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ZFudgeColumn=8 ZFudgeTunnel=15 ; ICBM launcher [ICBM] Name=Missile Launcher Category=Support Prerequisite=NAWEAP,NARADR Strength=500 Armor=light TechLevel=-1 Sight=7 Speed=6 Owner=Nod AllowedToStartInMultiplayer=no Turret=no Cost=1400 Points=30 ROT=5 DeploysInto=GAICBM Crusher=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys DebrisTypes=TIRE DebrisMaximums=4 ZFudgeColumn=18 ZFudgeTunnel=18 ; repair vehicle [REPAIR] Name=Mobile Repair Vehicle Category=Support Prerequisite=NODFACTORY Primary=RepairBullet Strength=200 Armor=light TechLevel=7 Sight=5 Speed=6 ; Dropped from 8 Owner=Nod AllowedToStartInMultiplayer=no Turret=no Cost=1000 Points=30 ROT=5 Crusher=yes Crewed=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=3 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SmallGreySSys ; The repair bot should not have a spark particle system in here! GuardRange=8 SpecialThreatValue=1 ZFudgeColumn=10 ZFudgeTunnel=14 ; advanced mobile artillery ; This unit needs a primary weapon type to allow targetting but it isn't actually ; allowed to fire unless deployed. [ART2] Name=Artillery FireAngle=42 Prerequisite=NODFACTORY,NARADR Primary=155mm Category=LRFS Strength=300 ;DeployTime=1.0 Turret=no DeploysInto=GAARTY Armor=light TechLevel=6 Sight=9 Speed=5 Owner=Nod AllowedToStartInMultiplayer=no Cost=975 Points=35 CrateGoodie=yes ROT=2 Crusher=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 NoMovingFire=yes DeployToFire=yes VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Crusher ThreatPosed=10 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=SELF_HEAL ZFudgeColumn=10 ZFudgeTunnel=14 ; Weed-eater vehicle [WEED] Name=Weed Eater Prerequisite=NODFACTORY,NAWAST ToProtect=yes Category=Support Strength=600 Armor=heavy Dock=NAWAST TechLevel=10 Sight=4 Weeder=yes Speed=5 Owner=Nod AllowedToStartInMultiplayer=no PipScale=Tiberium Storage=7 Cost=1400 Points=55 ROT=5 Crusher=yes Crewed=yes SelfHealing=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons ThreatAvoidanceCoefficient=.6 DamageParticleSystems=SparkSys,SmallGreySSys ImmuneToVeins=yes ZFudgeColumn=7 ZFudgeTunnel=12 ; harvester [HARV] Name=Harvester Prerequisite=FACTORY,PROC Nominal=yes ToProtect=yes Category=Support Explodes=yes Strength=1000 Armor=heavy Dock=PROC Harvester=yes UndeploysInto=HORV ;this specifies the "harvester without back" unit TechLevel=1 Sight=4 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no PipScale=Tiberium CrateGoodie=yes Storage=28 Cost=1400 Points=55 ROT=5 Crusher=yes AutoCrush=yes Crewed=yes SelfHealing=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=1 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons ThreatAvoidanceCoefficient=.65 DamageParticleSystems=SparkSys,SmallGreySSys ImmuneToVeins=yes ZFudgeColumn=9 ZFudgeTunnel=14 ZFudgeBridge=7 ; harvester without back [HORV] Name=Harvester Nominal=yes ToProtect=yes Category=Support Strength=1000 Armor=heavy Dock=PROC Harvester=yes TechLevel=-1 Sight=4 Speed=8 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=1400 Points=25 ROT=5 Crusher=yes Crewed=yes SelfHealing=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=1 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons ThreatAvoidanceCoefficient=1 DamageParticleSystems=SparkSys,SmallGreySSys ; Mobile Construction Vehicle [MCV] Name=Mobile Construction Vehicle Prerequisite=FACTORY,TECH Strength=1000 Category=Support Armor=heavy DeploysInto=GACNST TechLevel=10 Sight=6 Speed=3 Owner=GDI,Nod CrateGoodie=no ;[BASEUNIT] already handles this Cost=2500 Points=60 ROT=5 Crewed=yes Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys SpecialThreatValue=1 ZFudgeColumn=12 ZFudgeTunnel=15 AllowedToStartInMultiplayer=no ;BASEUNIT is a dummy unit that can never actually appear ingame, ;but instead it's always swapped with the MCV of the receiving player's faction. ;All MCVs that BASEUNIT can be swapped out with are to be specified after HarvesterUnit=, following the harvesters. [BASEUNIT] Image=MCV Name=Mobile Construction Vehicle CrateGoodie=yes ; Amphibious APC [APC] Name=Amphibious APC Prerequisite=GDIFACTORY,GAPILE Strength=200 Category=Transport Armor=heavy DeployTime=.022 TechLevel=6 Sight=5 PipScale=Passengers Speed=8 CrateGoodie=yes Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Crusher=yes Passengers=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 DebrisTypes=TIRE DebrisMaximums=6 SpeedType=Amphibious Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=AmphibiousCrusher ThreatPosed=10 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys SpecialThreatValue=1 ZFudgeColumn=10 ZFudgeTunnel=13 ; School Bus [BUS] Name=School Bus Strength=100 Nominal=yes Category=Transport DeployTime=.022 Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Crusher=yes Points=25 ROT=5 Passengers=20 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= MaxDebris=4 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.9 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ; Train Locomotive [LOCOMOTIVE] Name=Locomotive Nominal=yes Image=MONOENG Strength=100 Category=Transport DeployTime=.022 Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 Crusher=yes Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Passengers=2 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= MaxDebris=5 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.9 MovementRestrictedTo=Railroad SlowdownDistance=700 DeaccelerationFactor=0.001 AccelerationFactor=0.01 IsTrain=yes MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ; Train car [TRAINCAR] Name=Train Car Nominal=yes Image=MONOCAR Strength=100 Category=Transport DeployTime=.022 Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 Crusher=yes Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Passengers=10 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= MaxDebris=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.9 MovementRestrictedTo=Railroad Passive=yes IsTrain=yes MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ; Cargo car for train [CARGOCAR] Name=Cargo Car Nominal=yes Image=CARGOCAR Strength=100 Crusher=yes Category=Transport DeployTime=.022 Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Passengers=10 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= MaxDebris=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.9 MovementRestrictedTo=Railroad Passive=yes IsTrain=yes MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys CarriesCrate=yes ; pickup truck [PICK] Name=Pickup Truck Strength=100 Nominal=yes Category=Transport Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Passengers=2 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= MaxDebris=4 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ; Civilian automobile [CAR] Name=Automobile Strength=100 Category=Transport Nominal=yes Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Passengers=4 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= MaxDebris=4 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ; Small Visceroid [VISC_SML] Name=Baby Visceroid Nominal=yes Insignificant=yes Image=VISSML Strength=200 Category=Civilian Armor=light TechLevel=-1 Sight=0 Speed=8 Owner=Civilian AllowedToStartInMultiplayer=no Cost=1 Points=50 ROT=16 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 TiberiumHeal=yes Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Normal SmallVisceroid=yes ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes Trainable=no NonVehicle=yes ; Large Visceroid [VISC_LRG] Name=Adult Visceroid Insignificant=yes Image=VISLRG Nominal=yes AltImage=VISLGATK Strength=500 Category=Civilian Armor=heavy TechLevel=-1 Sight=0 Speed=8 TiberiumHeal=yes Owner=Civilian AllowedToStartInMultiplayer=no Cost=1 Points=50 ROT=16 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Normal LargeVisceroid=yes ThreatPosed=20 ; This value MUST be 0 for all building addons Primary=SlimeAttack GuardRange=5 ImmuneToVeins=yes Trainable=no NonVehicle=yes ; Hunter-Seeker Droid [GHUNTER] Name=GDI Hunter-Seeker Image=GGHUNT Strength=500 Insignificant=yes Category=AFV Primary=SuicideBomb Prerequisite=GAPLUG2 Armor=light TechLevel=-1 Sight=7 Speed=25 FlightLevel=400 Owner=GDI AllowedToStartInMultiplayer=no Cost=1000 Points=50 ROT=16 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 ThreatPosed=10 ; This value MUST be 0 for all building addons GuardRange=5 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly HunterSeeker=yes AlternateSpeed=10 ; this value is just used when exiting the war factory AlternateFlightLevel=50 ; this value is just used when exiting the war factory Selectable=false IgnoresFirestorm=yes ; Hunter-Seeker Droid [NHUNTER] Name=Nod Hunter-Seeker Image=GGHUNT Strength=500 Insignificant=yes Category=AFV Primary=SuicideBomb Prerequisite=NATMPL Armor=light TechLevel=-1 Sight=7 Speed=25 FlightLevel=400 Owner=Nod AllowedToStartInMultiplayer=no Cost=1000 Points=50 ROT=16 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 ThreatPosed=10 ; This value MUST be 0 for all building addons GuardRange=5 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly HunterSeeker=yes AlternateSpeed=10 ; this value is just used when exiting the war factory AlternateFlightLevel=50 ; this value is just used when exiting the war factory Selectable=false IgnoresFirestorm=yes ; Recreational Vehicle [WINI] Name=Recreational Vehicle Nominal=yes Strength=200 Category=Transport Armor=light TechLevel=-1 Sight=5 PipScale=Passengers Speed=8 CrateGoodie=yes Owner=GDI AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Passengers=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=4.0 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ImmuneToVeins=yes ; Medium Mech [MMCH] Name=Titan WalkRate=2 Image=MMCH Prerequisite=GDIFACTORY Primary=120mm Strength=400 Category=AFV Armor=heavy Turret=yes IsTilter=no TargetLaser=yes TooBigToFitUnderBridge=true TechLevel=3 Sight=8 Speed=4 CrateGoodie=yes Crusher=yes Owner=GDI Cost=800 Points=25 ROT=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={55D141B8-DB94-11d1-AC98-006008055BB5} MovementZone=Destroyer ThreatPosed=40 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys DamageSmokeOffset=100, 100, 275 Weight=3.5 EliteAbilities=SENSORS Accelerates=false ZFudgeColumn=8 ZFudgeTunnel=13 ZFudgeBridge=2 ; Mammoth Mk. II [HMEC] Name=Mammoth Mk.II Prerequisite=GDIFACTORY,GATECH Primary=MechRailgun Secondary=MammothTusk Strength=1200 ;800 Category=AFV Armor=heavy TechLevel=10 Sight=8 Speed=3 Owner=GDI AllowedToStartInMultiplayer=no Cost=3000 Trainable=no SelfHealing=yes Points=25 ROT=3 Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={55D141B8-DB94-11d1-AC98-006008055BB5} MovementZone=Destroyer ThreatPosed=80 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys DamageSmokeOffset=300, 300, 425 TiltsWhenCrushes=false BuildLimit=1 Weight=3.5 Accelerates=false ZFudgeColumn=12 ZFudgeTunnel=15 ZFudgeBridge=25 ; Small Mech [SMECH] Name=Wolverine Prerequisite=GDIFACTORY Primary=AssaultCannon Strength=175 Category=AFV Armor=light Turret=no IsTilter=no TooBigToFitUnderBridge=true TechLevel=2 Sight=6 Speed=7 CrateGoodie=yes Owner=GDI Cost=500 Points=25 ROT=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=15-I000,15-I006,15-I040,15-I042 VoiceMove=15-I024,15-I044 VoiceAttack=15-I006,15-I046 VoiceFeedback= MaxDebris=2 Locomotor={55D141B8-DB94-11d1-AC98-006008055BB5} MovementZone=Normal ThreatPosed=15 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys EliteAbilities=VEIN_PROOF Accelerates=false ImmuneToVeins=yes ; Attack Cycle [BIKE] Name=Attack Cycle Prerequisite=NODFACTORY Primary=BikeMissile Category=Recon Strength=150 Armor=wood Turret=no IsTilter=yes TechLevel=5 Sight=5 Speed=12 CrateGoodie=yes Owner=Nod Cost=600 Points=25 ROT=8 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=2 DebrisTypes=TIRE DebrisMaximums=2 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Destroyer ThreatPosed=20 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Elite=HoverMissile EliteAbilities=VEIN_PROOF ; Attack Buggy [BGGY] Name=Attack Buggy Prerequisite=NODFACTORY Primary=RaiderCannon Category=Recon Strength=220 Armor=light Turret=no IsTilter=yes TechLevel=2 Sight=6 Speed=10 CrateGoodie=yes Owner=Nod Cost=500 Points=25 ROT=8 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=3 DebrisTypes=TIRE DebrisMaximums=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Normal ThreatPosed=10 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys EliteAbilities=CRUSHER ImmuneToVeins=yes ; Subterranean APC [SAPC] Name=Subterranean APC Prerequisite=NODFACTORY,NATECH Strength=175 MoveToShroud=no Category=Transport DeployTime=.022 Armor=heavy Turret=no IsTilter=yes TechLevel=6 Sight=5 PipScale=Passengers Speed=5 CrateGoodie=yes Owner=Nod AllowedToStartInMultiplayer=no Cost=800 Points=25 ROT=5 Crusher=yes Passengers=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={4A582743-9839-11d1-B709-00A024DDAFD1} MovementZone=Subterannean ThreatPosed=10 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 SpecialThreatValue=1 ZFudgeColumn=7 ZFudgeTunnel=13 ; Subterranean Tank [SUBTANK] Name=Devil's Tongue Prerequisite=NODFACTORY,NATECH Primary=FireballLauncher MoveToShroud=no Strength=300 Category=AFV DeployTime=.022 TypeImmune=yes Armor=light Turret=no IsTilter=yes TechLevel=7 Sight=5 Speed=5 CrateGoodie=yes Owner=Nod AllowedToStartInMultiplayer=no Cost=750 Points=25 ROT=6 Crusher=yes NoMovingFire=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={4A582743-9839-11d1-B709-00A024DDAFD1} MovementZone=Subterannean ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=SELF_HEAL AutoCrush=no Accelerates=false ZFudgeColumn=10 ZFudgeTunnel=14 ; Disruptor [SONIC] Name=Disruptor Prerequisite=GDIFACTORY,GATECH Primary=SonicZap Strength=500 TypeImmune=yes Armor=heavy Category=AFV IsTilter=yes TechLevel=9 Turret=yes Sight=7 Speed=4 CrateGoodie=yes Owner=GDI Cost=1300 Points=25 ROT=4 Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= AllowedToStartInMultiplayer=no MaxDebris=5 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Destroyer ThreatPosed=60 ; This value MUST be 0 for all building addons NoMovingFire=true ; This MUST be set to true for the sonic tank DamageParticleSystems=SparkSys,SmallGreySSys EliteAbilities=EXPLODES ZFudgeColumn=12 ZFudgeTunnel=15 ; Tick Tank [TTNK] Name=Tick Tank Category=AFV Prerequisite=NODFACTORY Primary=90mm Strength=350 Armor=light TechLevel=3 CrateGoodie=yes Sight=5 Speed=6 Owner=Nod Cost=800 Points=40 ROT=5 Crusher=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Destroyer DeploysInto=GATICK ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=SENSORS Elite=120mmx AccelerationFactor=0.01 ZFudgeColumn=8 ZFudgeTunnel=13 ; Stealth tank [STNK] Name=Stealth Tank Prerequisite=NODFACTORY,NATECH Primary=Dragon Strength=180 ; w250 Armor=light Category=AFV Turret=no IsTilter=yes TechLevel=8 Sight=5 Speed=6 CrateGoodie=yes Owner=Nod AllowedToStartInMultiplayer=no Cost=1100 Points=25 ROT=5 Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=3 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Cloakable=yes CloakingSpeed=5 MovementZone=Destroyer ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=EXPLODES ZFudgeColumn=8 ZFudgeTunnel=13 ; Cyborg Reaper [REAPER] Name=Cyborg Reaper Category=AFV Prerequisite=NATECH,NODFACTORY Primary=QuadLauncher Secondary=WebLauncher Strength=400 ;was 350 Armor=light TechLevel=-1 Sight=7 Speed=5 Owner=Nod Cost=1100 Points=30 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Explosion=REAPRDIE ;TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=60-N100,60-N102,60-N104 VoiceMove=60-N106,60-N108,60-N110 VoiceAttack=60-N112,60-N114,60-N116 VoiceFeedback= ;SPIDDIE1 Crushable=no IsTilter=yes FireAngle=10 SpeedType=Creep NonVehicle=yes CrateGoodie=yes AllowedToStartInMultiplayer=no ImmuneToVeins=yes TiberiumProof=yes TiberiumHeal=yes EliteAbilities=CRUSHER Accelerates=false ; Tiberium Jellyfish [JFISH] Name=Tiberium Floater Insignificant=yes Image=FLOATER Nominal=yes Strength=500 Category=Civilian Armor=light TechLevel=-1 Sight=5 Speed=10 TiberiumHeal=yes TiberiumProof=yes ImmuneToVeins=yes Owner=Civilian AllowedToStartInMultiplayer=no Cost=1 Points=50 ROT=16 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Locomotor={3DC0B295-6546-11D3-80B0-00902792494C} MovementZone=AmphibiousDestroyer ThreatPosed=20 ; This value MUST be 0 for all building addons GuardRange=5 NonVehicle=yes Jellyfish=yes SpeedType=Hover Primary=Tentacle CrateGoodie=no Trainable=no ; Juggernaut [JUGG] Name=Juggernaut Category=AFV Prerequisite=GDIFACTORY,GARADR Image=JUGGER Primary=Jugg90mm Strength=350 Armor=light TechLevel=-1 Sight=9 Speed=5 Owner=GDI Cost=950 Points=40 ROT=5 Crusher=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={55D141B8-DB94-11d1-AC98-006008055BB5} MovementZone=Destroyer DeploysInto=DJUGG ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=SENSORS AccelerationFactor=0.01 NoMovingFire=yes DeployToFire=yes CrateGoodie=yes AllowedToStartInMultiplayer=no ; Limpet drone mine [LIMPET] Name=Limpet Drone Image=LIMPED IsLimpetDrone=yes Owner=GDI,Nod Category=AFV Prerequisite=FACTORY,RADAR Strength=100 Cost=550 ;was 700 Speed=8 Sight=5 Armor=none Points=50 TechLevel=-1 DeploysInto=DLIMPET Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 ThreatPosed=10 ; This value MUST be 0 for all building addons GuardRange=0 SpeedType=Hover Locomotor={4A582742-9839-11d1-B709-00A024DDAFD1} MovementZone=AmphibiousDestroyer AlternateSpeed=10 CrateGoodie=no AllowedToStartInMultiplayer=no Trainable=no VoiceSelect=LIMPQ3,LIMPQ4 VoiceMove=LIMPC3,LIMPC4 VoiceAttack=LIMPC3,LIMPC4 VoiceFeedback=LIMPC3,LIMPC4 ;Mobile EM-Pulse [MOBILEMP] Name=Mobile EM-Pulse Image=M_EMP Prerequisite=GDIFACTORY,NAPULS Strength=800 ;was 600 Category=Support Armor=heavy TechLevel=-1 Sight=6 Speed=7 ;was 3 Owner=GDI Cost=1000 ;was 1400 Points=60 ROT=5 Crewed=yes Crusher=yes TypeImmune=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys SpecialThreatValue=1 ZFudgeColumn=12 ZFudgeTunnel=15 CrateGoodie=yes AllowedToStartInMultiplayer=no PipScale=Charge MaxCharge=1800 ; was 1200 StartCharge=0; IsMobileEMP=true Trainable=no ;Mobile EM-Pulse (Precharged) [CMOBILEMP] Name=Mobile EM-Pulse (Charged) Image=M_EMP Strength=800 Category=Support Armor=heavy TechLevel=-1 Sight=6 Speed=7 ;was 3 Owner=GDI Cost=1000 Points=60 ROT=5 Crewed=yes Crusher=yes TypeImmune=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Crusher ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys SpecialThreatValue=1 ZFudgeColumn=12 ZFudgeTunnel=15 CrateGoodie=yes AllowedToStartInMultiplayer=no PipScale=Charge MaxCharge=1800 ; was 1200 StartCharge=1800; IsMobileEMP=true Trainable=no ; Mobile Stealth Generator [SGEN] Name=Mobile Stealth Generator Image=SGEN Prerequisite=NODFACTORY,NASTLH Strength=200 ;was 250 Armor=light Category=AFV Turret=no IsTilter=yes TechLevel=-1 Sight=5 Speed=6 Owner=Nod Cost=1600 ;was 1800 Points=25 ROT=5 Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=3 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Crusher ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=EXPLODES ZFudgeColumn=8 ZFudgeTunnel=13 ;PipScale=Charge ;MaxCharge=400 DeploysInto=MSTL CrateGoodie=yes AllowedToStartInMultiplayer=no Trainable=no Crewed=no ; Mobile Weapons Factory (GDI) [MOBWARG] Name=Mobile War Factory Image=MWAR_NOD Prerequisite=GAWEAP,GAPLUG BuildLimit=1 TechLevel=-1 Category=Support Strength=800 Armor=heavy Sight=6 Speed=3 Owner=GDI Cost=1800 Points=60 ROT=5 Crewed=yes Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys SpecialThreatValue=1 ZFudgeColumn=12 ZFudgeTunnel=15 DeploysInto=DGWEAP CrateGoodie=no AllowedToStartInMultiplayer=no Trainable=no ; Mobile Weapons Factory (Nod) [MOBWARN] Name=Fist of Nod Image=MWAR_NOD Prerequisite=NAWEAP,NATMPL BuildLimit=1 TechLevel=-1 Category=Support Strength=800 Armor=heavy Sight=6 Speed=3 Owner=Nod Cost=1800 Points=60 ROT=5 Crewed=yes Crusher=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=6 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} Weight=3.5 MovementZone=Normal ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys SpecialThreatValue=1 ZFudgeColumn=12 ZFudgeTunnel=15 DeploysInto=DNWEAP CrateGoodie=no AllowedToStartInMultiplayer=no Trainable=no ; Retro Flame Tank [FLMTNK] Name=Flame Tank Image=FTNK Category=AFV Prerequisite=NODFACTORY Primary=FireballLauncher Strength=300 Armor=light TechLevel=-1 CrateGoodie=yes AllowedToStartInMultiplayer=no Sight=5 Speed=6 Owner=Civilian Cost=700 Points=40 ROT=5 Crusher=yes Crewed=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 VoiceFeedback= MaxDebris=4 Locomotor={4A582741-9839-11d1-B709-00A024DDAFD1} MovementZone=Destroyer ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys Weight=3.5 EliteAbilities=EXPLODES AccelerationFactor=0.01 ZFudgeColumn=8 ZFudgeTunnel=13 ; Core Defender [DEFENDER] Name=Core Defender Category=AFV Prerequisite= Strength=10000 ;was 2500 Armor=heavy TechLevel=-1 Sight=9 Speed=5 Owner=Civilian Cost=2000 Points=40 ROT=5 Crusher=yes Crewed=no Weight=3.5 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= Locomotor={55D141B8-DB94-11d1-AC98-006008055BB5} MovementZone=Destroyer ThreatPosed=50 DamageParticleSystems=SparkSys,SmallGreySSys MaxDebris=30 ;was 10 Explosion=DEFD_EXP AllowedToStartInMultiplayer=no IsCoreDefender=yes WalkRate=4 Primary=DEFOB DamageSmokeOffset=0,0,550 Trainable=yes EliteAbilities=SENSORS ImmuneToVeins=yes TiberiumProof=yes TiberiumHeal=yes SelfHealing=yes NoMovingFire=true ; ******* Infantry Types ******* ; This is the list of infantry types. Each infantry type listed ; here should also have a matching data section that specifies ; its data values. The purpose of this list is to identify infantry ; types that can't be implicitly determined by examining other ; entries in this rules file. [InfantryTypes] 00=E1 01=E2 02=E3 03=MEDIC 04=ELCAD 05=CTECH 06=HUEY 07=ENGINEER ;index 7 is used in nod5a.map 08=CYBORG 09=JUMPJET 10=CHAMSPY 11=UMAGON 12=GHOST 13=CYC2 14=MHIJACK 15=CIV1 16=CIV2 17=CIV3 18=CIV4 19=CIV5 20=CIV6 21=MUTANT 22=MWMN 23=MUTANT3 24=TRATOS 25=OXANNA 26=SLAV 27=DOGGIE ;28=WEEDGUY [CYC2] Name=Cyborg Commando Category=Soldier Primary=CyCannon ;Secondary=FireballLauncher Prerequisite=NAHAND,NATMPL CrushSound=SQUISHY2 Crushable=no TiberiumProof=yes TiberiumHeal=yes Strength=500 Fearless=yes Armor=heavy TechLevel=10 Sight=7 Pip=white Speed=4 Owner=Nod Cost=2000 Trainable=no Cyborg=yes Points=5 AllowedToStartInMultiplayer=no VoiceSelect=23-I000,23-I002,23-I004,23-I006 VoiceMove=23-I008,23-I010,23-I012,23-I016 VoiceAttack=23-I014,23-I018,23-I020,23-I022 VoiceFeedback= VoiceDie=22-N104,22-N106,22-N108 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=50 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys BuildLimit=1 ImmuneToVeins=yes IsWebImmune=true ; Chameleon Spy [CHAMSPY] Name=Chameleon Spy Category=Soldier Prerequisite=NAHAND,NATECH CrushSound=SQUISH6 Strength=120 Armor=none TechLevel=-1 Agent=yes Sight=9 Speed=6 Infiltrate=yes Owner=Nod AllowedToStartInMultiplayer=no Cost=700 Pip=white Points=5 VoiceSelect=21-I000,21-I002,21-I004 VoiceMove=21-I010,21-I012,21-I016 VoiceAttack=21-I010,21-I012,21-I022 VoiceFeedback=21-I000,21-I002 VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry Cloakable=yes CloakingSpeed=10 ThreatPosed=0 ; This value MUST be 0 for all building addons SpecialThreatValue=1 ImmuneToVeins=yes ; rifle soldier [E1] Name=Light Infantry Category=Soldier Primary=Minigun Prerequisite=BARRACKS CrushSound=SQUISH6 Strength=125 Pip=green Armor=none TechLevel=1 Sight=5 Speed=5 Owner=GDI,Nod Cost=120 Points=5 VoiceSelect=15-I000,15-I004,15-I012,15-I048 VoiceMove=15-I018,15-I024,15-I044 VoiceAttack=15-I044,15-I050,15-I044,15-I046 VoiceFeedback=15-I058,15-I064 VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons Elite=M1Carbine EliteAbilities=SCATTER ImmuneToVeins=yes [E2] Name=Disc Thrower Category=Soldier Primary=Grenade Prerequisite=GAPILE CrushSound=SQUISH6 Strength=150 Armor=none TechLevel=2 Pip=green Sight=7 Speed=4 Owner=GDI Cost=200 Points=5 VoiceSelect=15-I000,15-I004,15-I012,15-I048 VoiceMove=15-I018,15-I024,15-I044 VoiceAttack=15-I044,15-I050,15-I044,15-I046 VoiceFeedback=15-I058,15-I064 VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=InfantryDestroyer ThreatPosed=15 ; This value MUST be 0 for all building addons EliteAbilities=SCATTER ImmuneToVeins=yes Explodes=yes [E3] Name=Rocket Infantry Category=Soldier Primary=BAZOOKA Prerequisite=NAHAND CrushSound=SQUISH6 Strength=100 Armor=none TechLevel=2 Pip=green Sight=7 Speed=4 Owner=Nod Cost=250 Points=5 VoiceSelect=15-I000,15-I032,15-I048 VoiceMove=15-I008,15-I014,15-I026 VoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060 VoiceFeedback=15-I058,15-I064 VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=InfantryDestroyer ThreatPosed=20 ; This value MUST be 0 for all building addons EliteAbilities=SCATTER ImmuneToVeins=yes ;; Hack to make MultiMissile & MobileEMPulseWeapon work! Don't change data! ;[WEEDGUY] ;Name=Chem Spray Infantry ;Category=Soldier ;Primary=MultiCluster ;Secondary=DualRockets ;Elite=MobileEMPulseWeapon ;Prerequisite=BARRACKS ;TiberiumProof=yes ;CrushSound=SQUISHY2 ;Strength=130 ;Storage=7 ;Pip=green ;Fearless=yes ;Armor=none ;TechLevel=-1 ;Sight=4 ;Speed=3 ;Owner=GDI ;AllowedToStartInMultiplayer=no ;Cost=300 ;Points=5 ;VoiceSelect=15-I000,15-I006,15-I040,15-I042 ;VoiceMove=15-I024,15-I044 ;VoiceAttack=15-I006,15-I046 ;VoiceFeedback=15-I058,15-I064 ;VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 ;Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} ;PhysicalSize=1 ;MovementZone=Infantry ;ThreatPosed=0 ; This value MUST be 0 for all building addons [MEDIC] Name=Medic Category=Soldier Primary=Heal Prerequisite=GAPILE CrushSound=SQUISHY2 Strength=125 Armor=none TechLevel=4 Sight=6 Speed=4 SelfHealing=yes Pip=red Owner=GDI AllowedToStartInMultiplayer=no Cost=600 Points=5 VoiceSelect=20-I000,20-I004,20-I006 VoiceMove=20-I008,20-I010,20-I012 VoiceAttack=20-I016,20-I018,20-I020 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons GuardRange=8 SpecialThreatValue=1 ImmuneToVeins=yes [UMAGON] Name=Umagon Category=Soldier Primary=Sniper CrushSound=SQUISH6 TiberiumProof=yes TiberiumHeal=yes Strength=150 Armor=light TechLevel=-1 Sight=7 Speed=5 Owner=GDI Pip=white AllowedToStartInMultiplayer=no Cost=400 Points=5 Trainable=no VoiceSelect=10-I000,10-I002,10-I004,10-I006 VoiceMove=10-I016,10-I020,10-I022 VoiceAttack=10-I024,10-I026,10-I028,10-I030 VoiceFeedback= VoiceDie=DEDGIRL1,DEDGIRL2,DEDGIRL2,DEDGIRL4 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=15 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [GHOST] Name=Ghost Stalker Category=Soldier Prerequisite=GAPILE,GATECH Primary=LtRail C4=yes TiberiumHeal=yes CrushSound=SQUISHY2 TiberiumProof=yes Strength=200 Armor=light TechLevel=10 Pip=white Sight=6 Speed=4 Owner=GDI AllowedToStartInMultiplayer=no Cost=1750 Points=5 Trainable=no VoiceSelect=14-I000,14-I002,14-I004 VoiceMove=14-I008,14-I010,14-I012,14-I014 VoiceAttack=14-I008,14-I010,14-I014,14-I016 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=25 ; This value MUST be 0 for all building addons SpecialThreatValue=1 BuildLimit=1 ImmuneToVeins=yes [CYBORG] Name=Cyborg Category=Soldier Prerequisite=NAHAND Primary=Vulcan3 CrushSound=SQUISHY2 Crushable=no TiberiumProof=yes TiberiumHeal=yes Fearless=yes Cyborg=yes Pip=white Strength=300 ; w350 Armor=light TechLevel=4 Sight=5 Speed=4 Owner=Nod Cost=650 Points=5 VoiceSelect=22-I000,22-I002,22-I006 VoiceMove=22-I008,22-I010,22-I014,22-I016,22-I020 VoiceAttack=22-I008,22-I010,22-I012,22-I018 VoiceFeedback= VoiceDie=22-N104,22-N106,22-N108 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry DamageParticleSystems=SparkSys ThreatPosed=15 ; This value MUST be 0 for all building addons EliteAbilities=STRONGER ImmuneToVeins=yes ;Nod Elite Cadre Soldier [ELCAD] Name=Elite Cadre Category=Soldier Image=SLAV Primary=Vulcan3 Prerequisite=NAHAND TiberiumProof=yes CrushSound=SQUISH6 Strength=175 Fearless=yes Armor=light Pip=white TechLevel=-1 Sight=4 Speed=4 Owner=Nod Cost=300 AllowedToStartInMultiplayer=no Points=5 VoiceSelect=15-I032,15-I048 VoiceMove=15-I008,15-I014,15-I026 VoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [MUTANT] Name=Mutant Category=Soldier Primary=Vulcan CrushSound=SQUISH6 TiberiumProof=yes TiberiumHeal=yes Strength=50 Armor=none TechLevel=-1 Sight=4 Speed=4 Owner=GDI,Nod Cost=100 Pip=white Points=5 AllowedToStartInMultiplayer=no VoiceSelect=15-I032,15-I048 VoiceMove=15-I008,15-I014,15-I026 VoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [MWMN] Name=Mutant Soldier Category=Soldier Primary=Vulcan CrushSound=SQUISH6 TiberiumProof=yes TiberiumHeal=yes Strength=50 Armor=none TechLevel=-1 Sight=4 Speed=4 Pip=white Owner=GDI,Nod Cost=100 Points=5 AllowedToStartInMultiplayer=no VoiceSelect=11-I000,11-I002,11-I004,11-I006 VoiceMove=11-I008,11-I010,11-I012 VoiceAttack=11-I012,11-I010,11-I016 VoiceFeedback= VoiceDie=DEDGIRL1,DEDGIRL2,DEDGIRL2,DEDGIRL4 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [MUTANT3] Name=Mutant Sergeant Category=Soldier Primary=Vulcan CrushSound=SQUISHY2 TiberiumProof=yes TiberiumHeal=yes Strength=50 Pip=white Armor=none TechLevel=-1 Sight=4 Speed=4 Owner=GDI,Nod Cost=100 Points=5 AllowedToStartInMultiplayer=no VoiceSelect=15-I032,15-I048 VoiceMove=15-I008,15-I014,15-I026 VoiceAttack=15-I008,15-I014,15-I026,15-I050,15-I060 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [TRATOS] Name=Tratos Category=Soldier Primary=none CrushSound=SQUISHY2 TiberiumProof=yes TiberiumHeal=yes Strength=200 Armor=none TechLevel=-1 Sight=4 Speed=5 Owner=GDI,Nod Pip=white Cost=100 Points=5 AllowedToStartInMultiplayer=no VoiceSelect=13-I000,13-I002,13-I004,13-I006 VoiceMove=13-I008,13-I010,13-I012,13-I014 VoiceAttack=13-I016,13-I018,13-I020 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [OXANNA] Name=Oxanna Category=Soldier Primary=Vulcan CrushSound=SQUISH6 ;TiberiumProof=yes ;Crim: this has gotta be an oversight ;TiberiumHeal=yes Strength=50 Armor=none TechLevel=-1 Sight=4 Speed=4 AllowedToStartInMultiplayer=no Owner=GDI,Nod Cost=100 Pip=white Points=5 VoiceSelect=11-I000,11-I002,11-I004,11-I006 VoiceMove=11-I008,11-I010,11-I012 VoiceAttack=11-I014,11-I016,11-I018 VoiceFeedback= VoiceDie=DEDGIRL1,DEDGIRL2,DEDGIRL2,DEDGIRL4 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [DOGGIE] Name=Tiberian Fiend Nominal=yes Category=Soldier Doggie=yes Primary=FiendShard CrushSound=SQUISHY2 Strength=250 Armor=light Fearless=no TiberiumProof=yes TiberiumHeal=yes Trainable=no Pip=green TechLevel=-1 Sight=4 Speed=8 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=100 Points=5 VoiceSelect= VoiceMove= VoiceAttack= VoiceFeedback= VoiceDie=FIEND1 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=25 ; This value MUST be 0 for all building addons ImmuneToVeins=yes [ENGINEER] Name=Engineer Category=Soldier Primary=none Prerequisite=BARRACKS CrushSound=SQUISH6 Strength=100 Armor=none TechLevel=2 Sight=4 Speed=4 Pip=yellow Engineer=yes Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=500 Points=5 VoiceSelect=19-I000,19-I002,19-I006 VoiceMove=19-I010,19-I016 VoiceAttack=19-I018,19-I016 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons SpecialThreatValue=1 ; this should be between 0 and 1 ImmuneToVeins=yes GuardRange=9 [JUMPJET] Name=Jumpjet Infantry Category=Soldier JumpJet=yes Primary=JumpCannon Prerequisite=GAPILE,GARADR Crushable=no Strength=120 Fearless=yes Armor=light TechLevel=6 Sight=6 Pip=green Speed=5 Owner=GDI AllowedToStartInMultiplayer=no Cost=600 Points=5 VoiceSelect=15-I000,15-I004,15-I012,15-I048 VoiceMove=15-I018,15-I024,15-I044 VoiceAttack=15-I044,15-I050,15-I044,15-I046 VoiceFeedback=15-I058,15-I064 VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={92612C46-F71F-11d1-AC9F-006008055BB5} PhysicalSize=1 MovementZone=Fly ; This needs to be None, like aircraft ThreatPosed=15 ; This value MUST be 0 for all building addons EliteAbilities=RADAR_INVISIBLE [MHIJACK] Name=Mutant Hijacker Category=Soldier Prerequisite=NAHAND,NATMPL Primary=none Crushable=no ;CrushSound=SQUISHY2 Strength=300 Armor=none TiberiumProof=yes TiberiumHeal=yes TechLevel=10 Sight=6 Speed=7 Pip=white Owner=Nod Cost=1850 AllowedToStartInMultiplayer=no Points=5 Trainable=no VoiceSelect=24-I000,24-I002,24-I004,24-I006 VoiceMove=24-I008,24-I010,24-I012,24-I014 VoiceAttack=24-I016,24-I018,24-I020,24-I022,24-I024 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 VehicleThief=yes MovementZone=Infantry ThreatPosed=20 ; This value MUST be 0 for all building addons SpecialThreatValue=1 ; this should be between 0 and 1 GuardRange=6 BuildLimit=1 ImmuneToVeins=yes [SLAV] Name=Slavick Category=Soldier Primary=none CrushSound=SQUISH6 Strength=300 Armor=none Pip=white TechLevel=-1 Sight=4 Speed=4 Owner=GDI,Nod Cost=100 AllowedToStartInMultiplayer=no Points=5 VoiceSelect=12-I000,12-I002,12-I004 VoiceMove=12-I006,12-I008,12-I010 VoiceAttack=12-I012,12-I014,12-I016 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=10 ; This value MUST be 0 for all building addons ImmuneToVeins=yes ; civilians [CIV1] Name=Civilian Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 ;Ammo=10 Fraidycat=yes Civilian=yes Nominal=yes Pip=white VoiceSelect=67-N100,67-N102 VoiceMove=67-N104,67-N106,67-N108 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [CIV2] Name=Civilian Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISHY2 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 ;Ammo=10 Fraidycat=yes Civilian=yes Nominal=yes Pip=white VoiceSelect=68-N100,68-N102,68-N104 VoiceMove=68-N106,68-N108,68-N110 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [CIV3] Name=Civilian Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 ;Ammo=10 Fraidycat=yes Civilian=yes Nominal=yes Pip=white VoiceSelect=69-N100,69-N102,69-N104 VoiceMove=69-N106,69-N108,69-N110 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [CIV4] Name=Civilian Image=CIV1 Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 ;Ammo=10 Fraidycat=no Civilian=yes Nominal=yes Pip=white VoiceSelect=67-N100,67-N102 VoiceMove=67-N104,67-N106,67-N108 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [CIV5] Name=Civilian Image=CIV2 Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 ;Ammo=10 Fraidycat=no Civilian=yes Nominal=yes Pip=white VoiceSelect=68-N100,68-N102,68-N104 VoiceMove=68-N106,68-N108,68-N110 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [CIV6] Name=Civilian Image=CIV3 Category=Civilian Strength=50 Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 ;Ammo=10 Fraidycat=no Civilian=yes Nominal=yes Pip=white VoiceSelect=69-N100,69-N102,69-N104 VoiceMove=69-N106,69-N108,69-N110 VoiceAttack=BOOP VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [CTECH] Name=Technician Image=CIV3 Category=Civilian Strength=50 Primary=Pistola Armor=none TechLevel=-1 CrushSound=SQUISH6 Insignificant=yes Sight=2 Speed=5 Owner=GDI,Nod AllowedToStartInMultiplayer=no Cost=10 Points=1 Ammo=10 Reload=80 Fraidycat=no Civilian=yes Nominal=yes Pip=white VoiceSelect=70-N000,70-N002,70-N004 VoiceMove=70-N006,70-N008,70-N010 VoiceAttack=70-N014,70-N016,70-N018 VoiceFeedback= VoiceDie=DEDMAN1,DEDMAN2,DEDMAN2,DEDMAN4,DEDMAN5,DEDMAN6 Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry ThreatPosed=0 ; This value MUST be 0 for all building addons ImmuneToVeins=yes EliteAbilities=SCATTER [HUEY] Name=Huey the Infected Cyborg Image=CYBORG Category=Soldier Prerequisite= Primary=Vulcan3 CrushSound=SQUISHY2 Crushable=no TiberiumProof=yes TiberiumHeal=yes Fearless=yes Cyborg=yes Pip=white Strength=300 ; w350 Armor=light TechLevel=-1 Sight=5 Speed=4 Owner= Cost=650 Points=5 VoiceSelect=22-I000,22-I002,22-I006 VoiceMove=22-I008,22-I010,22-I014,22-I016,22-I020 VoiceAttack=22-I008,22-I010,22-I012,22-I018 VoiceFeedback= VoiceDie=22-N104,22-N106,22-N108 ;Crim: was DEDMAN1...6, not fitting Locomotor={4A582744-9839-11d1-B709-00A024DDAFD1} PhysicalSize=1 MovementZone=Infantry DamageParticleSystems=SparkSys ThreatPosed=15 ; This value MUST be 0 for all building addons EliteAbilities=STRONGER ImmuneToVeins=yes ; ******* Aircraft Types ******* ; This lists all of the aircraft types in the game. Each aircraft ; type should have a matching section that specifies the data it ; requires. [AircraftTypes] 0=ORCA 1=APACHE 2=TRNSPORT 3=ORCAB 4=SCRIN 5=DPOD 6=ORCATRAN 7=DSHP ; Drop Pod [DPOD] Name=Drop Pod Category=AirPower Prerequisite=afld Primary=Vulcan2 Strength=60 Selectable=no Armor=light TechLevel=-1 Sight=0 RadarInvisible=yes Speed=16 Owner=GDI Cost=10 Points=20 ROT=5 Ammo=5 Passengers=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I030,30-I034,30-I036 Locomotor={4A582745-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly ThreatPosed=10 ; This value MUST be 0 for all building addons ; Dropship [DSHP] Name=Dropship Prerequisite=GAWEAP Strength=200 Category=AirLift Armor=heavy Landable=yes TechLevel=-1 Sight=3 PipScale=Passengers Speed=18 PitchSpeed=.4 RadarInvisible=yes Owner=GDI Cost=0 Points=25 ROT=5 Selectable=yes Passengers=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I034,30-I036 IsDropship=yes FlightLevel=1600 MaxDebris=9 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly ThreatPosed=0 ; This value MUST be 0 for all building addons ;Dock=GADROP SlowdownDistance=2000 DamageParticleSystems=SparkSys,SmallGreySSys LegalTarget=no AuxSound1=DROPUP1 ;Taking off AuxSound2=DROPDWN1 ;Landing ; ORCA Fighter [ORCA] Name=Orca Fighter Prerequisite=GAHPAD Primary=Hellfire Strength=200 Category=AirPower Armor=light TechLevel=5 Sight=2 RadarInvisible=no Landable=yes MoveToShroud=no Dock=GAHPAD,NAHPAD PipScale=Ammo Speed=20 PitchSpeed=.16 Owner=GDI Cost=1000 Points=20 ROT=5 Ammo=5 Crewed=yes GuardRange=30 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I030,30-I034,30-I036 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly ThreatPosed=20 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys AuxSound1=ORCAUP1 ;Taking off AuxSound2=ORCADWN1 ;Landing ; ORCA Bomber [ORCAB] Name=Orca Bomber Prerequisite=GAHPAD,GATECH Primary=Bomb Strength=260 Category=AirPower Armor=light TechLevel=8 Sight=2 RadarInvisible=no Landable=yes MoveToShroud=no Dock=GAHPAD,NAHPAD PipScale=Ammo Speed=12 PitchSpeed=.16 Owner=GDI Cost=1600 Points=20 ROT=5 Ammo=2 Crewed=yes GuardRange=30 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I030,30-I034,30-I036 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly ThreatPosed=25 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys AuxSound1=ORCAUP1 ;Taking off AuxSound2=ORCADWN1 ;Landing EliteAbilities=RADAR_INVISIBLE ; Orca Transport [ORCATRAN] Name=Orca Transport Prerequisite=GAHPAD Strength=200 Category=AirPower Armor=light TechLevel=-1 Sight=2 RadarInvisible=no Landable=yes PipScale=Passengers Passengers=5 Speed=9 PitchSpeed=1.1 Owner=GDI Cost=1200 Points=20 ROT=5 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I034,30-I036 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly DamageParticleSystems=SparkSys,SmallGreySSys AuxSound1=ORCAUP1 ;Taking off AuxSound2=ORCADWN1 ;Landing ThreatPosed=0 SpecialThreatValue=1 ; Carryall [TRNSPORT] Name=Carryall Prerequisite=GAHPAD,GADEPT Strength=175 Category=AirPower Armor=light TechLevel=9 Sight=2 RadarInvisible=no Carryall=yes Landable=yes MoveToShroud=no Speed=16 PitchSpeed=1.1 Owner=GDI Cost=750 Points=20 ROT=5 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I034,30-I036 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly DamageParticleSystems=SparkSys,SmallGreySSys AuxSound1=DROPUP1 ;Taking off AuxSound2=DROPDWN1 ;Landing ThreatPosed=0 SpecialThreatValue=1 ; Banshee Fighter [SCRIN] Name=Banshee Prerequisite=NAHPAD,NATECH Primary=Proton Strength=280 Category=AirPower Armor=light TechLevel=9 Sight=2 RadarInvisible=no Landable=yes MoveToShroud=no Dock=NAHPAD,GAHPAD PipScale=Ammo Speed=18 PitchSpeed=.9 Owner=Nod Cost=1500 Points=20 ROT=3 Ammo=3 Crewed=yes GuardRange=30 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 VoiceSelect=32-I000 VoiceMove=32-I004 VoiceAttack=32-I002,32-I004,32-I006 VoiceFeedback=32-I008 VoiceDie=32-I008 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys AuxSound1=DROPUP1 ;Taking off AuxSound2=DROPDWN1 ;Landing EliteAbilities=RADAR_INVISIBLE ; Apache Chopper [APACHE] Name=Harpy Prerequisite=NAHPAD Primary=HarpyClaw Strength=225 Category=AirPower Armor=light TechLevel=5 Sight=2 RadarInvisible=yes Landable=yes MoveToShroud=no Dock=NAHPAD,GAHPAD PipScale=Ammo Speed=14 PitchSpeed=.16 Owner=Nod Cost=1000 Points=20 ROT=5 Ammo=12 Crewed=yes GuardRange=30 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 VoiceSelect=30-I000,30-I002,30-I004,30-I006 VoiceMove=30-I014,30-I016,30-I018,30-I022 VoiceAttack=30-I022,30-I030,30-I034,30-I036 Locomotor={4A582746-9839-11d1-B709-00A024DDAFD1} MovementZone=Fly ThreatPosed=15 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys ; ******* Building Types ******* ; This lists all the buildings types in the game. Each of these ; types will have a specific section in this file that gives the ; particulars about that building type. [BuildingTypes] ; Least threat, highest threat, nearest, farthest 000=GAPOWR ; 000, 65536, 131072, 196608 001=PROC ; 001, 65537, 131073, 196609 - 131073 used in ai(fs).ini 002=GASILO ; 002, 65538, 131074, 196610 - 131074 used in ai(fs).ini 003=GAPILE ; 003, 65539, 131075, 196611 004=GAPLUG ; 004, 65540, 131076, 196612 - 131076 used in ai(fs).ini 005=GACTWR ; 005, 65541, 131077, 196613 006=GAVULC ; 006, 65542, 131078, 196614 007=GASAND ; 007, 65543, 131079, 196615 008=GAFIRE ; 008, 65544, 131080, 196616 009=GADEPT ; 009, 65545, 131081, 196617 010=GATECH ; 010, 65546, 131082, 196618 011=GAWEAP ; 011, 65547, 131083, 196619 012=GACNST ; 012, 65548, 131084, 196620 - 131084 used in ai(fs).ini 013=GAHPAD ; 013, 65549, 131085, 196621 014=NAPOWR ; 014, 65550, 131086, 196622 015=NATECH ; 015, 65551, 131087, 196623 016=NAHAND ; 016, 65552, 131088, 196624 017=NAAPWR ; 017, 65553, 131089, 196625 018=GAWALL ; 018, 65554, 131090, 196626 019=CABHUT ; 019, 65555, 131091, 196627 020=NAPULS ; 020, 65556, 131092, 196628 021=GAGATE_A ; 021, 65557, 131093, 196629 022=GAGATE_B ; 022, 65558, 131094, 196630 023=NAWEAP ; 023, 65559, 131095, 196631 024=NASTLH ; 024, 65560, 131096, 196632 025=GALITE ; 025, 65561, 131097, 196633 026=REDLAMP ; 026, 65562, 131098, 196634 027=GRENLAMP ; 027, 65563, 131099, 196635 028=BLUELAMP ; 028, 65564, 131100, 196636 029=YELWLAMP ; 029, 65565, 131101, 196637 030=PURPLAMP ; 030, 65566, 131102, 196638 031=INORANLAMP ; 031, 65567, 131103, 196639 032=INGRNLMP ; 032, 65568, 131104, 196640 033=INREDLMP ; 033, 65569, 131105, 196641 034=NAWALL ; 034, 65570, 131106, 196642 035=INBLULMP ; 035, 65571, 131107, 196643 036=NATMPL ; 036, 65572, 131108, 196644 037=NAGATE_A ; 037, 65573, 131109, 196645 038=NAGATE_B ; 038, 65574, 131110, 196646 039=NAWAST ; 039, 65575, 131111, 196647 040=NAOBEL ; 040, 65576, 131112, 196648 041=NAMISL ; 041, 65577, 131113, 196649 - 131113 used in ai(fs).ini 042=GAPOWRUP ; 042, 65578, 131114, 196650 043=NAPOST ; 043, 65579, 131115, 196651 044=NAFNCE ; 044, 65580, 131116, 196652 045=NALASR ; 045, 65581, 131117, 196653 046=NASAM ; 046, 65582, 131118, 196654 047=CITY01 ; 047, 65583, 131119, 196655 048=CITY02 ; 048, 65584, 131120, 196656 049=CITY03 ; 049, 65585, 131121, 196657 050=CITY04 ; 050, 65586, 131122, 196658 051=CITY05 ; 051, 65587, 131123, 196659 052=CITY06 ; 052, 65588, 131124, 196660 053=CITY07 ; 053, 65589, 131125, 196661 054=CITY08 ; 054, 65590, 131126, 196662 055=CITY09 ; 055, 65591, 131127, 196663 056=CITY10 ; 056, 65592, 131128, 196664 057=CITY11 ; 057, 65593, 131129, 196665 058=CITY12 ; 058, 65594, 131130, 196666 059=CITY13 ; 059, 65595, 131131, 196667 060=CITY14 ; 060, 65596, 131132, 196668 061=CITY15 ; 061, 65597, 131133, 196669 062=CITY16 ; 062, 65598, 131134, 196670 063=CITY17 ; 063, 65599, 131135, 196671 064=CITY18 ; 064, 65600, 131136, 196672 065=CAHOSP ; 065, 65601, 131137, 196673 066=GASPOT ; 066, 65602, 131138, 196674 067=CTDAM ; 067, 65603, 131139, 196675 068=NARADR ; 068, 65604, 131140, 196676 069=GAROCK ; 069, 65605, 131141, 196677 070=INGALITE ; 070, 65606, 131142, 196678 071=INYELWLAMP ; 071, 65607, 131143, 196679 072=INPURPLAMP ; 072, 65608, 131144, 196680 073=GAPLUG1 ; 073, 65609, 131145, 196681 074=GAPLUG2 ; 074, 65610, 131146, 196682 075=GAPLUG3 ; 075, 65611, 131147, 196683 076=GAFSDF ; 076, 65612, 131148, 196684 077=GARADR ; 077, 65613, 131149, 196685 078=BBOARD01 ; 078, 65614, 131150, 196686 079=BBOARD02 ; 079, 65615, 131151, 196687 080=BBOARD03 ; 080, 65616, 131152, 196688 081=BBOARD04 ; 081, 65617, 131153, 196689 082=BBOARD05 ; 082, 65618, 131154, 196690 083=BBOARD06 ; 083, 65619, 131155, 196691 084=BBOARD07 ; 084, 65620, 131156, 196692 085=BBOARD08 ; 085, 65621, 131157, 196693 086=BBOARD09 ; 086, 65622, 131158, 196694 087=BBOARD10 ; 087, 65623, 131159, 196695 088=BBOARD11 ; 088, 65624, 131160, 196696 089=BBOARD12 ; 089, 65625, 131161, 196697 090=BBOARD13 ; 090, 65626, 131162, 196698 091=BBOARD14 ; 091, 65627, 131163, 196699 092=BBOARD15 ; 092, 65628, 131164, 196700 093=BBOARD16 ; 093, 65629, 131165, 196701 094=NEGLAMP ; 094, 65630, 131166, 196702 095=NEGRED ; 095, 65631, 131167, 196703 096=ABAN01 ; 096, 65632, 131168, 196704 097=ABAN02 ; 097, 65633, 131169, 196705 098=ABAN03 ; 098, 65634, 131170, 196706 099=ABAN04 ; 099, 65635, 131171, 196707 100=ABAN05 ; 100, 65636, 131172, 196708 101=ABAN06 ; 101, 65637, 131173, 196709 102=ABAN07 ; 102, 65638, 131174, 196710 103=ABAN08 ; 103, 65639, 131175, 196711 104=ABAN09 ; 104, 65640, 131176, 196712 105=ABAN10 ; 105, 65641, 131177, 196713 106=ABAN11 ; 106, 65642, 131178, 196714 107=ABAN12 ; 107, 65643, 131179, 196715 108=ABAN13 ; 108, 65644, 131180, 196716 109=ABAN14 ; 109, 65645, 131181, 196717 110=ABAN15 ; 110, 65646, 131182, 196718 111=ABAN16 ; 111, 65647, 131183, 196719 112=ABAN17 ; 112, 65648, 131184, 196720 113=ABAN18 ; 113, 65649, 131185, 196721 114=CITY19 ; 114, 65650, 131186, 196722 115=CITY20 ; 115, 65651, 131187, 196723 116=CITY21 ; 116, 65652, 131188, 196724 117=NTPYRA ; 117, 65653, 131189, 196725 118=CITY22 ; 118, 65654, 131190, 196726 119=CTVEGA ; 119, 65655, 131191, 196727 120=GADPSA ; 120, 65656, 131192, 196728 121=CA0001 ; 121, 65657, 131193, 196729 122=CA0002 ; 122, 65658, 131194, 196730 123=CA0003 ; 123, 65659, 131195, 196731 124=CA0004 ; 124, 65660, 131196, 196732 125=CA0005 ; 125, 65661, 131197, 196733 126=CA0006 ; 126, 65662, 131198, 196734 127=CA0007 ; 127, 65663, 131199, 196735 128=CA0008 ; 128, 65664, 131200, 196736 129=CA0009 ; 129, 65665, 131201, 196737 130=CA0010 ; 130, 65666, 131202, 196738 131=CA0011 ; 131, 65667, 131203, 196739 132=CA0012 ; 132, 65668, 131204, 196740 133=CA0013 ; 133, 65669, 131205, 196741 134=CA0014 ; 134, 65670, 131206, 196742 135=CA0015 ; 135, 65671, 131207, 196743 136=CA0016 ; 136, 65672, 131208, 196744 137=CA0017 ; 137, 65673, 131209, 196745 138=CA0018 ; 138, 65674, 131210, 196746 139=CA0019 ; 139, 65675, 131211, 196747 140=CA0020 ; 140, 65676, 131212, 196748 141=CA0021 ; 141, 65677, 131213, 196749 142=CAARMR ; 142, 65678, 131214, 196750 143=GACSAM ; 143, 65679, 131215, 196751 144=GATICK ; 144, 65680, 131216, 196752 145=CAPYR01 ; 145, 65681, 131217, 196753 146=CAPYR02 ; 146, 65682, 131218, 196754 147=CAPYR03 ; 147, 65683, 131219, 196755 148=CACRSH01 ; 148, 65684, 131220, 196756 149=CACRSH02 ; 149, 65685, 131221, 196757 150=CACRSH03 ; 150, 65686, 131222, 196758 151=CACRSH04 ; 151, 65687, 131223, 196759 152=CACRSH05 ; 152, 65688, 131224, 196760 153=CAARAY ; 153, 65689, 131225, 196761 154=GAICBM ; 154, 65690, 131226, 196762 155=GAOLDCC1 ; 155, 65691, 131227, 196763 156=GAOLDCC2 ; 156, 65692, 131228, 196764 157=GAOLDCC3 ; 157, 65693, 131229, 196765 158=GAOLDCC4 ; 158, 65694, 131230, 196766 159=GAOLDCC5 ; 159, 65695, 131231, 196767 160=GAOLDCC6 ; 160, 65696, 131232, 196768 161=GAARTY ; 161, 65697, 131233, 196769 162=TSTLAMP ; 162, 65698, 131234, 196770 163=NAHPAD ; 163, 65699, 131235, 196771 164=GAKODK ; 164, 65700, 131236, 196772 165=NAMNTK ; 165, 65701, 131237, 196773 166=UFO ; 166, 65702, 131238, 196774 167=AMMOCRAT ; 167, 65703, 131239, 196775 168=GAPAVE ; 168, 65704, 131240, 196776 169=GAGREEN ; 169, 65705, 131241, 196777 170=INORNGLAMP ; 170, 65706, 131242, 196778 171=GAPLUG4 ; 171, 65707, 131243, 196779 172=DJUGG ; 172, 65708, 131244, 196780 173=DLIMPET ; 173, 65709, 131245, 196781 174=C_KODIAK ; 174, 65710, 131246, 196782 175=DGWEAP ; 175, 65711, 131247, 196783 176=DNWEAP ; 176, 65712, 131248, 196784 177=MSTL ; 177, 65713, 131249, 196785 178=DDEFD ; 178, 65714, 131250, 196786 179=AAOB ; 179, 65715, 131251, 196787 180=CORE ; 180, 65716, 131252, 196788 181=CROB ; 181, 65717, 131253, 196789 182=KODIAK ; 182, 65718, 131254, 196790 ; advanced tech center [GATECH] Name=GDI Tech Center Prerequisite=GAWEAP,GARADR Strength=500 Armor=wood TechLevel=6 Adjacent=2 Sight=6 Owner=GDI Cost=1500 Points=85 Power=-200 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=5 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=1500, 1055, 815 AIBuildThis=yes TogglePower=no ; GDI weapons factory [GAWEAP] Name=GDI War Factory Image=GAWEAP WeaponsFactory=yes Prerequisite=PROC,GAPILE Factory=UnitType DeployTime=.044 Strength=1000 Armor=heavy TechLevel=2 Sight=4 Adjacent=2 Owner=GDI Cost=2000 Points=80 Power=-30 Capturable=true Crewed=yes Bib=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=408, 880, 435 AIBuildThis=yes ; NOD weapons factory [NAWEAP] Name=Nod War Factory WeaponsFactory=yes Prerequisite=PROC,NAHAND Factory=UnitType DeployTime=.044 Strength=1000 Adjacent=2 Armor=heavy TechLevel=2 Sight=4 Owner=Nod Cost=2000 Points=80 Power=-30 Capturable=true Crewed=yes Bib=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 NaturalSmokeLocation=-12,0,370 MaxDebris=8 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=395, 750, 410 AIBuildThis=yes ; construction yard [GACNST] Name=Construction Yard ConstructionYard=yes Strength=1000 Armor=heavy TechLevel=-1 Adjacent=2 Factory=BuildingType UndeploysInto=MCV Sight=6 Owner=GDI,Nod Cost=2500 Points=80 Power=0 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=10 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=1470, 1060, 1078 AIBuildThis=yes TogglePower=no [GADPSA] Name=Deployed Sensor Array TechLevel=-1 Strength=600 Points=50 Cost=950 Sight=8 Power=0 Armor=wood SensorArray=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 VoiceSelect=25-I000 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 MaxDebris=2 UndeploysInto=LPST BaseNormal=no ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=500, 500, 400 HasRadialIndicator=true RadialColor=0,200,0 CloakRadiusInCells=25 TogglePower=no Owner=GDI,Nod Crewed=yes [GAICBM] Name=Deployed ICBM TechLevel=-1 Strength=400 Points=50 Power=0 Armor=wood ICBMLauncher=yes ;This key is repurposed by the oil_derricks patch of ts-patches and thus doesn't work anymore. It's replaced by SensorArray=yes, which does the same thing. SensorArray=yes ;Makes the unit face south-east before deploying, prevents it from jumping up by a cell after deploying and prevents it from disappearing after un-deploying in missions. ;SuperWeapon=MultiSpecial Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 UndeploysInto=ICBM BaseNormal=no ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=500, 500, 400 TogglePower=no Owner=Nod Crewed=yes [GATICK] Name=Deployed Tick Tank TechLevel=-1 ;was 5 Strength=350 Points=50 Cost=800 Power=0 Armor=concrete Sight=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 UndeploysInto=TTNK BaseNormal=no VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 Crewed=yes Primary=90mm Elite=120mmx Turret=yes ROT=5 TickTank=yes TurretAnim=TTNKTUR TurretAnimIsVoxel=true TurretAnimX=4 TurretAnimY=10 TurretAnimZAdjust=-20 ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys DamageSmokeOffset=500, 500, 400 Trainable=yes TogglePower=no EliteAbilities=SENSORS HasStupidGuardMode=false Owner=Nod [GAARTY] Name=Deployed Artillery TechLevel=-1 ;was 8 Strength=300 Points=50 Cost=975 Power=0 Armor=light Sight=9 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 UndeploysInto=ART2 BaseNormal=no VoiceSelect=15-I004,15-I006,15-I008,15-I010,15-I012,15-I038,15-I040,15-I048 VoiceMove=15-I014,15-I016,15-I018,15-I020,15-I022,15-I024,15-I060 VoiceAttack=15-I026,15-I032,15-I044,15-I046,15-I050 Crewed=yes Primary=155mm Turret=yes ROT=5 Artillary=yes TurretAnim=ART2TUR TurretAnimIsVoxel=true TurretAnimX=-8 TurretAnimY=15 TurretAnimZAdjust=-20 ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=500, 500, 400 EliteAbilities=SELF_HEAL Trainable=yes TogglePower=no HasStupidGuardMode=false Owner=Nod ; Deployed Juggernaut [DJUGG] Name=Deployed Juggernaut Image=DJUGG TechLevel=-1 Strength=400 ;was 350 Points=50 Cost=975 Power=0 Armor=light Sight=9 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 UndeploysInto=JUGG BaseNormal=no VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 Crewed=yes Primary=Jugg90mm ROT=5 IsJuggernaut=yes Turret=yes TurretAnim=DJUGG_A TurretAnimIsVoxel=false TurretAnimX=0 TurretAnimY=0 TurretAnimZAdjust=-30 BarrelAnimIsVoxel=true VoxelBarrelFile=DJUGGBAR VoxelBarrelOffsetToPitchPivotPoint=15,0,-8 VoxelBarrelOffsetToRotatePivotPoint=2,0,0 VoxelBarrelOffsetToBuildingPivotPoint=4,2,3 VoxelBarrelOffsetToBarrelEnd=350,75,0 VoxelBarrelScale=.75 StartFacing=4 ; DIR_S = 4 << 5 StartPitch=2 ; DIR_E = 2 << 5 ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=0,0,125 EliteAbilities=SELF_HEAL Trainable=yes TogglePower=no HasStupidGuardMode=false Owner=GDI ; Deployed Limpet Drone [DLIMPET] Name=Limpet Mine Strength=100 Points=50 Cost=700 Sight=5 Power=0 Armor=none Cloakable=yes CloakingSpeed=10 Primary=LIMP TechLevel=-1 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 BaseNormal=no UndeploysInto=LIMPET Unsellable=true IsLimpetMine=true Owner=GDI,Nod ; Mobile weapons factory (GDI) [DGWEAP] Name=Mobile War Factory Image=MWAR WeaponsFactory=yes Factory=UnitType DeployTime=.044 Strength=800 Armor=heavy TechLevel=-1 BuildLimit=1 Sight=4 BaseNormal=no Cost=2000 Points=80 Power=0 Capturable=true Crewed=yes Bib=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=408, 880, 435 AIBuildThis=no UndeploysInto=MOBWARG IsMobileWar=yes Owner=GDI ; Mobile weapons factory (Nod) [DNWEAP] Name=Fist of Nod Image=MWAR WeaponsFactory=yes Factory=UnitType DeployTime=.044 Strength=800 Armor=heavy TechLevel=-1 BuildLimit=1 Sight=4 BaseNormal=no Cost=2000 Points=80 Power=0 Capturable=true Crewed=yes Bib=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=408, 880, 435 AIBuildThis=no UndeploysInto=MOBWARN IsMobileWar=yes VoiceSelect=25-I000,25-I002,25-I004,25-I006 VoiceMove=25-I012,25-I014,25-I016,25-I018,25-I022 VoiceAttack=25-I014,25-I022,25-I024,25-I026 Owner=Nod ; Deployed Mobile Stealth Generator [MSTL] Name=Mobile Stealth Generator Image=MSTL CloakGenerator=yes CloakRadiusInCells=6 HasRadialIndicator=true RadialColor=255,0,0 Strength=200 ;was 600 Armor=wood TechLevel=-1 Adjacent=2 Sight=6 Cost=1600 ; w2000 Points=60 Power=0 Powered=false Capturable=false Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=5 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=0,0,100 AIBuildThis=no ; UndeploysInto=SGEN IsMobileStealth=yes Owner=Nod Crewed=no BaseNormal=no ; Tiberium Refinery [PROC] Name=Tiberium Refinery ;Image=NAREFN ;Crim: Rules image ignores (pre)production anim Z, so we'll use image in art [PROC] instead Refinery=yes Bib=yes Prerequisite=POWER Strength=900 Adjacent=2 Armor=heavy TechLevel=1 FreeUnit=HARV DockUnload=yes Sight=6 Owner=GDI,Nod Cost=2000 Points=80 Power=-30 Storage=80 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 HalfDamageSmokeLocation1=0,0,0 MaxDebris=8 PipScale=Tiberium ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=410, 100, 165 AIBuildThis=yes TogglePower=no ; storage silo [GASILO] Name=Tiberium Silo Prerequisite=PROC Strength=300 Armor=wood TechLevel=1 Adjacent=2 Sight=2 Owner=GDI,Nod Cost=150 Points=25 Power=-10 Storage=60 Explodes=yes Capturable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=2 PipScale=Tiberium ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys DamageSmokeOffset=700, 700, 500 TogglePower=no ; helipad [GAHPAD] Name=Helipad Prerequisite=GARADR Strength=600 Armor=wood Adjacent=2 TechLevel=5 Sight=5 UnitReload=yes Helipad=yes Owner=GDI Cost=500 Points=70 Power=-10 Factory=AircraftType Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=75, 270, 140 AIBuildThis=yes HasStupidGuardMode=false ; helipad [NAHPAD] Name=Helipad Prerequisite=NARADR Strength=600 Armor=wood Adjacent=2 TechLevel=5 Sight=5 UnitReload=yes Helipad=yes Owner=Nod Cost=500 Points=70 Power=-10 Factory=AircraftType Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=275, 60, 140 AIBuildThis=yes HasStupidGuardMode=false ; GDI Radar [GARADR] Name=Radar Prerequisite=PROC Strength=1000 Radar=yes Armor=wood TechLevel=3 Adjacent=2 Sight=10 Owner=GDI Cost=1000 Points=60 Power=-40 Powered=true Capturable=true Sensors=yes Crewed=yes Upgrades=0 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=6 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=440, 200, 200 AIBuildThis=yes ; GDI Communications tower [GAPLUG] Name=GDI Upgrade Center Prerequisite=PROC,GATECH Strength=1000 Radar=no Armor=wood TechLevel=10 Adjacent=2 Sight=6 Owner=GDI Cost=1000 Points=60 Power=-150 Powered=true Capturable=true Sensors=yes Crewed=yes Upgrades=2 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=6 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=800, 550, 400 AIBuildThis=yes SpecialThreatValue=1 IsPlug=true ; NOD Stealth Generator [NASTLH] Name=Stealth Generator Prerequisite=PROC,NATECH CloakGenerator=yes CloakRadiusInCells=12 HasRadialIndicator=true RadialColor=255,0,0 Strength=600 Armor=wood TechLevel=9 Adjacent=2 Sight=6 Owner=Nod Cost=2500 ; w2000 Points=60 Power=-350 Powered=true Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=5 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=450, 200, 150 AIBuildThis=yes ; commented out so that it's easier to debug base building ; gdi power plant [GAPOWR] Name=GDI Power Plant Strength=750 Armor=wood TechLevel=1 Adjacent=2 Sight=4 Owner=GDI Cost=300 Points=40 Power=100 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Upgrades=2 MaxDebris=6 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=300, 300, 450 TogglePower=no ; nod power plant [NAPOWR] Name=NOD Power Plant Strength=750 Armor=wood TechLevel=1 Sight=4 Adjacent=2 Owner=Nod Cost=300 Points=40 Power=100 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=6 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=450, 200, 300 TogglePower=no ; Nod advanced power plant [NAAPWR] Name=Advanced Power Plant Prerequisite=NAWEAP Strength=750 Armor=wood TechLevel=7 Adjacent=2 Sight=4 Owner=Nod Cost=500 Points=40 Power=200 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=290, 570, 320 TogglePower=no ; NOD Tech Center [NATECH] Name=NOD Tech Center Prerequisite=NAWEAP,NARADR Strength=500 Armor=wood TechLevel=6 Adjacent=2 Sight=6 Owner=Nod Cost=1500 Points=85 Power=-100 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=5 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=200, 325, 200 AIBuildThis=yes TogglePower=no ; NOD Barracks [NAHAND] Name=Hand Of Nod Prerequisite=POWER Strength=800 Armor=wood TechLevel=1 Adjacent=2 Sight=5 Owner=Nod Cost=300 Points=30 Power=-20 Factory=InfantryType Crewed=yes Capturable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons ExitCoord = 0,0,0 NODBarracks=yes DamageParticleSystems=SparkSys,SmallGreySmokeSys,BigGreySmokeSys DamageSmokeOffset=480, 96, 125 AIBuildThis=yes ; GDI Barracks [GAPILE] Name=Barracks Prerequisite=POWER Strength=800 Armor=wood Factory=InfantryType Adjacent=2 TechLevel=1 Sight=5 Owner=GDI Cost=300 Points=30 Power=-20 Crewed=yes Capturable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons ExitCoord = -64,64,0 GDIBarracks=yes DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=215, 395, 200 AIBuildThis=yes ; service depot [GADEPT] Name=Service Depot Prerequisite=FACTORY Strength=1100 Armor=wood TechLevel=7 Adjacent=2 Sight=5 UnitRepair=yes UnitReload=yes Owner=GDI ; removed from Nod side Cost=1200 Points=80 Power=-30 Capturable=true Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=270, 580, 260 AIBuildThis=yes HasStupidGuardMode=false ; Pavenent [GAPAVE] Name=Pavement Strength=150 Prerequisite=BARRACKS High=yes Armor=concrete TechLevel=6 Adjacent=3 Sight=0 Selectable=no Insignificant=yes Nominal=yes Owner=GDI,Nod Cost=75 BaseNormal=no Points=5 Repairable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=0 ; This value MUST be 0 for all building addons ToTile=pvclr01 ; green lat (not used) [GAGREEN] Name=Green Building Image=null Strength=150 Prerequisite=GAPILE High=yes Armor=concrete TechLevel=-1 Adjacent=3 Sight=0 Selectable=no Insignificant=yes Nominal=yes Owner=GDI,Nod Cost=100 BaseNormal=no Points=5 Repairable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=0 ; This value MUST be 0 for all building addons ToTile=Green01 ; sandbag wall [GASAND] Name=Sandbags Strength=250 Prerequisite=BARRACKS Armor=light CrushSound=SANDBAG1 Crushable=yes Wall=yes TechLevel=-1 Adjacent=4 Sight=0 Nominal=yes Selectable=no Owner=GDI,NOD Cost=125 ;25 BaseNormal=no Insignificant=yes Points=1 Repairable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=0 ; This value MUST be 0 for all building addons ;IsBase=no ;Crim: not a rules flag. Use BaseNormal. ; concrete wall [GAWALL] Name=Concrete Wall Strength=150 Prerequisite=GAPILE High=yes Armor=concrete TechLevel=6 Adjacent=4 Wall=yes Sight=1 Selectable=no Insignificant=yes Nominal=yes Owner=GDI Cost=250 ;50 BaseNormal=no Points=5 Repairable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=0 ; This value MUST be 0 for all building addons ;IsBase=no GuardRange=5 ; NOD wall [NAWALL] Name=Nod Wall Strength=150 Prerequisite=NAHAND High=yes Armor=concrete TechLevel=6 Adjacent=4 Wall=yes Sight=1 Selectable=no Insignificant=yes Nominal=yes Owner=Nod Cost=250 ;50 BaseNormal=no Points=5 Repairable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=0 ; This value MUST be 0 for all building addons ;IsBase=no GuardRange=5 ; Bridge repair hut [CABHUT] Name=Bridge repair hut Strength=2000 Immune=yes LegalTarget=no Nominal=yes TechLevel=-1 RadarInvisible=yes Repairable=true Selectable=no Insignificant=yes BridgeRepairHut=yes Adjacent=0 BaseNormal=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=0 ; This value MUST be 0 for all building addons ; Nod pulse cannon [NAPULS] Name=EMP Cannon Strength=500 Armor=heavy Prerequisite=Radar TechLevel=6 Sight=8 Adjacent=2 Owner=Nod,GDI Cost=1000 Turret=yes Points=50 Power=-150 Sensors=yes Crewed=yes ROT=12 EMPulseCannon=yes SuperWeapon=EMPulseSpecial Primary=EMPulseWeapon Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=5 TurretAnim=PULSCAN TurretAnimIsVoxel=true TurretAnimY=7 TurretAnimX=1 TurretAnimZAdjust=-100 ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=350, 125, 100 HasStupidGuardMode=false ; GDI Component Tower [GACTWR] Name=Component Tower Strength=500 Armor=light Prerequisite=GAPILE TechLevel=2 Sight=4 Adjacent=3 Owner=GDI Cost=200 Turret=yes Points=50 Power=-10 Sensors=yes BaseNormal=no Crewed=no ROT=12 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 TurretAnimZAdjust=-45 ;WST 6/18/99 was 50 but cause z buffer problem: Component towers turret animation depends on what the player attaches : HasSpotlight=false; MaxDebris=2 ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys DamageSmokeOffset=500, 500, 400 ;IsBase=no ;Crim: not a rules flag HasStupidGuardMode=false ; GDI gate in wall [GAGATE_A] Name=Gate Strength=350 Prerequisite=GAPILE Armor=heavy TechLevel=6 Selectable=yes Capturable=false Insignificant=yes Adjacent=4 Owner=GDI Cost=250 BaseNormal=no Points=50 Repairable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 Gate=yes DeployTime=.044 GateCloseDelay=.2 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys ;IsBase=no ; GDI gate in wall [GAGATE_B] Name=Gate Strength=350 Armor=heavy Prerequisite=GAPILE TechLevel=6 Adjacent=4 Selectable=yes Capturable=false Insignificant=yes Owner=GDI Cost=250 BaseNormal=no Points=50 Repairable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Gate=yes DeployTime=.044 GateCloseDelay=.2 MaxDebris=2 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys ;IsBase=no ; Nod gate in wall [NAGATE_A] Name=Gate Strength=350 Armor=heavy Prerequisite=NAHAND TechLevel=6 Adjacent=4 Selectable=yes Capturable=false Insignificant=yes Owner=Nod Cost=250 BaseNormal=no Points=50 Repairable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Gate=yes DeployTime=.044 GateCloseDelay=.2 MaxDebris=2 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys ;IsBase=no ; Nod gate in wall [NAGATE_B] Name=Gate Strength=350 Armor=heavy Prerequisite=NAHAND TechLevel=6 Adjacent=4 Selectable=yes Capturable=false Insignificant=yes Owner=Nod Cost=250 BaseNormal=no Points=50 Repairable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Gate=yes DeployTime=.044 GateCloseDelay=.2 MaxDebris=2 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys ;IsBase=no ; Light post [TSTLAMP] Name=AlphaLightPost Image=GALITE Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=no ; why would we want guys to come out of the lightpost? BNA 7/15/99 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys Insignificant=yes AlphaImage=ALPHATST ; Light post [GALITE] Name=Light Post Image=GALITE Strength=600 Armor=wood Owner=GDI,NOD Cost=200 TechLevel=-1 ; changed from 12 Nominal=yes Sight=0 Points=30 Power=0 Crewed=NO Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=5000 LightIntensity=0.2 LightRedTint=0.05 LightGreenTint=0.05 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys AlphaImage=NONE Powered=true ; Black Light post [NEGLAMP] Name=Negative Light Post Image=GALITE Strength=1000 Armor=wood TechLevel=-1 InvisibleInGame=yes Nominal=yes Sight=0 Points=30 Power=0 Crewed=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 LightVisibility=3500 LightIntensity=-0.15 LightRedTint=0.03 LightGreenTint=0.04 LightBlueTint=0.04 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys Insignificant=yes ; Light post [INGALITE] Name=Invisible Light Post Image=GALITE InvisibleInGame=yes Insignificant=yes Selectable=no Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=5000 LightIntensity=0.2 LightRedTint=0.05 LightGreenTint=0.05 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Light post [REDLAMP] Name=Red Light Post Image=GALITE Insignificant=yes Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=4000 LightIntensity=0.01 LightRedTint=1.5 LightGreenTint=0.01 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys AlphaImage=NONE Powered=true ; Light post [NEGRED] Name=Negative Red Light Image=GALITE Insignificant=yes Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=4000 LightIntensity=-0.05 LightRedTint=-1.5 LightGreenTint=0.01 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys ; Light post [GRENLAMP] Name=Green Light Post Image=GALITE Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=1.5 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys AlphaImage=NONE Powered=true Insignificant=yes ; Light post [BLUELAMP] Name=Blue Light Post Image=GALITE Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=0.01 LightBlueTint=0.7 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys AlphaImage=NONE Powered=true Insignificant=yes ; Light post [YELWLAMP] Name=Yellow Light Post Image=GALITE Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=4000 LightIntensity=.01 LightRedTint=1.5 LightGreenTint=1.5 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys Powered=true Insignificant=yes ; Light post [INYELWLAMP] Name=Invisible Yellow Light Post Image=GALITE Insignificant=yes Selectable=no InvisibleInGame=yes Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=4000 LightIntensity=.01 LightRedTint=1.5 LightGreenTint=1.5 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Light post [PURPLAMP] Name=Purple Light Post Image=GALITE Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=3000 LightIntensity=0.01 LightRedTint=2.0 LightGreenTint=0.01 LightBlueTint=2.0 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys AlphaImage=NONE Powered=true Insignificant=yes ; Light post [INPURPLAMP] Name=Invisible Purple Light Post Image=GALITE Selectable=no Insignificant=yes InvisibleInGame=yes Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 LightVisibility=3000 LightIntensity=0.01 LightRedTint=2.0 LightGreenTint=0.01 LightBlueTint=2.0 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Light post [INORANLAMP] Name=Orange Light Post Image=GALITE Selectable=no InvisibleInGame=yes Strength=600 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 LightVisibility=3000 LightIntensity=0.01 LightRedTint=2.0 LightGreenTint=1.4 LightBlueTint=0.3 DamageParticleSystems=SparkSys,LGSparkSys Powered=true Insignificant=yes [INORNGLAMP] Name=Invisible Orange Light Post Image=GALITE InvisibleInGame=yes Insignificant=yes Selectable=no Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=1 LightVisibility=3000 LightIntensity=0.01 LightRedTint=1.1 LightGreenTint=0.55 LightBlueTint=0.01 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Light post [INGRNLMP] Name=Invisible Green Light Post Image=GALITE Selectable=no InvisibleInGame=yes Insignificant=yes Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=1.5 LightBlueTint=0.01 DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Light post [INREDLMP] Name=Invisible Red Light Post Image=GALITE Selectable=no Insignificant=yes InvisibleInGame=yes Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 LightVisibility=4000 LightIntensity=0.01 LightRedTint=1.5 LightGreenTint=0.01 LightBlueTint=0.01 DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Invisible Light post [INBLULMP] Name=Invisible Blue Light Post Selectable=no InvisibleInGame=yes Insignificant=yes Image=GALITE Strength=6000 Armor=wood TechLevel=-1 Nominal=yes Sight=0 Points=30 Power=0 Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=0.01 LightBlueTint=0.7 DamageParticleSystems=SparkSys,LGSparkSys Powered=true ; Temple of NOD [NATMPL] Name=Temple of NOD Prerequisite=NATECH Strength=1000 Armor=wood TechLevel=10 Adjacent=3 Sight=6 Owner=Nod Cost=2000 Points=60 Power=-200 Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=9 ThreatPosed=0 ; This value MUST be 0 for all building addons SuperWeapon=HuntSeekSpecial DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=1210, 680, 400 AIBuildThis=yes IsTemple=yes ; pyramid of NOD [NTPYRA] Name=NOD Pyramid Strength=1500 Armor=heavy TechLevel=-1 Adjacent=2 Sight=0 Owner=Nod Cost=1000 Points=60 Power=-40 Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=9 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=1150, 660, 475 ;Kodiak [GAKODK] Name=GDI Kodiak Strength=1500 Armor=heavy TechLevel=-1 Adjacent=2 Sight=10 Owner=GDI Cost=1000 Points=60 Power=0 Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=9 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=1200, 880, 635 ;IsBase=no ;Crim: not a rules flag BaseNormal=no TogglePower=no ;Large Kodiak by Lin Kuei Ominae [KODIAK] Name=GDI Kodiak Strength=1750 Armor=heavy TechLevel=-1 Adjacent=2 Sight=10 Owner=GDI Cost=1000 Points=60 Power=0 Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=9 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=2400, 1960, 1535 ;IsBase=no BaseNormal=no TogglePower=no ;Montauk [NAMNTK] Name=NOD Montauk Strength=1500 Armor=heavy TechLevel=-1 Adjacent=2 Sight=0 Owner=NOD Cost=1000 Points=60 Power=0 Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=9 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=700, 1180, 800 ;IsBase=no BaseNormal=no TogglePower=no ; Dropship bay (obsolete) ;[GADROP] ;Name=Dropship Bay ;Prerequisite=DOME,GATECH ;Strength=3000 ;TechLevel=9 ;Adjacent=2 ;Sight=10 ;Owner=GDI ;Cost=1000 ;Points=60 ;Power=-40 ;Powered=true ;Capturable=true ;Sensors=yes ;Crewed=yes ;Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ;MaxDebris=8 ;ThreatPosed=0 ; This value MUST be 0 for all building addons ;DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys ;NOD Radar Facility [NARADR] Name=NOD Radar Prerequisite=PROC Strength=1000 Radar=yes Armor=wood TechLevel=3 Adjacent=2 Sight=10 Owner=NOD Cost=1000 Points=60 Power=-40 Powered=true Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=220, 390, 150 AIBuildThis=yes ; Nod tiberium waste facility [NAWAST] Name=Tiberium Waste Facility Prerequisite=NAMISL Strength=400 Armor=wood TechLevel=10 Adjacent=2 Sight=5 Owner=Nod Cost=1600 Points=60 Power=-40 FreeUnit=WEED Capturable=true Sensors=yes Crewed=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=400, 625, 200 Weeder=yes Bib=yes PipScale=Tiberium ;SuperWeapon=ChemicalSpecial AIBuildThis=yes TogglePower=no BuildLimit=1 ; NOD Obelisk [NAOBEL] Name=Obelisk of Light Prerequisite=NATECH Strength=725 Armor=wood TechLevel=9 Adjacent=2 Sight=8 Owner=Nod Cost=1500 Points=30 Power=-150 Crewed=yes Capturable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Primary=LaserFire Turret=no TurretAnim=NAOBEL_B TurretAnimZAdjust=-100 MaxDebris=4 ThreatPosed=40 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=355, 525, 225 IsBaseDefense=yes BaseNormal=no Powered=yes HasStupidGuardMode=false ; Nod missile silo [NAMISL] Name=Missile Silo SuperWeapon=MultiSpecial SuperWeapon2=ChemicalSpecial Prerequisite=NATECH Strength=1000 Armor=wood TechLevel=10 Adjacent=2 Sight=4 Owner=Nod Cost=1300 Points=30 Power=-50 Crewed=yes Capturable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=6 ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys AIBuildThis=yes SpecialThreatValue=1 NukeSilo=yes HasStupidGuardMode=false ; Vulcan cannon add-on for component tower [GAVULC] Name=Vulcan Cannon Image=GAVULC Prerequisite=GACTWR,GAPILE TechLevel=2 Armor=wood Sight=7 Owner=GDI Cost=150 Points=30 Power=-20 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gactwr PowersUpToLevel=1 Primary=VulcanTower Secondary=VulcanTower Turret=yes ThreatPosed=0 ; This value MUST be 0 for all building addons IsBaseDefense=yes ; Rocket launcher addon for component tower [GAROCK] Name=RPG Upgrade Image=GAROCK Prerequisite=GACTWR,GAPILE TechLevel=9 Armor=wood Sight=8 Owner=GDI Cost=600 Points=30 Power=-20 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gactwr PowersUpToLevel=2 Primary=RPGTower Turret=yes ThreatPosed=0 ; This value MUST be 0 for all building addons IsBaseDefense=yes ; SAM addon for component tower [GACSAM] Name=SAM Upgrade Image=GACSAM Prerequisite=GACTWR,GARADR TechLevel=5 Armor=wood Sight=10 Owner=GDI Cost=300 Points=30 Power=-30 Crewed=no Capturable=no Explosion=TWLT070 PowersUpBuilding=gactwr PowersUpToLevel=3 Primary=RedEye2 Secondary=RedEye2 Turret=yes ThreatPosed=0 ; This value MUST be 0 for all building addons IsBaseDefense=yes Powered=yes ; GDI Power plant upgrade. [GAPOWRUP] Name=Power Turbine Prerequisite=GAPOWR Image=GAPOWR_B TechLevel=7 Armor=wood Sight=1 Owner=GDI Cost=100 Points=30 Power=50 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gapowr PowersUpToLevel=-1 ThreatPosed=0 ; This value MUST be 0 for all building addons ; OBSOLETE ; Upgrade No. 1 for Upgrade Center [GAPLUG1] Name=Threat Rating Node Image=GAPLUG_D Prerequisite=GAPLUG TechLevel=-1 Armor=wood Sight=1 Owner=GDI Cost=500 Points=30 Power=-20 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gaplug PowersUpToLevel=-1 ThreatPosed=0 ; This value MUST be 0 for all building addons IsThreatRatingNode=true ; Upgrade No. 2 for Upgrade Center [GAPLUG2] Name=Seeker Control Image=GAPLUG_E Prerequisite=GAPLUG,GATECH,GAWEAP TechLevel=10 Armor=wood Sight=1 Owner=GDI Cost=1000 Points=30 Power=-50 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gaplug PowersUpToLevel=-1 ThreatPosed=0 ; This value MUST be 0 for all building addons SuperWeapon=HuntSeekSpecial AIBuildThis=yes ; Upgrade No. 3 for Upgrade Center [GAPLUG3] Name=Ion Cannon Uplink Image=GAPLUG_F Prerequisite=GAPLUG,GATECH TechLevel=10 Armor=wood Sight=1 Owner=GDI Cost=1500 Points=30 Power=-100 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gaplug PowersUpToLevel=-1 SuperWeapon=IonCannonSpecial ThreatPosed=0 ; This value MUST be 0 for all building addons AIBuildThis=yes ; Upgrade No. 4 for Upgrade Center [GAPLUG4] Name=Drop Pod Node Image=GAPLUG_D Prerequisite=GAPLUG TechLevel=11 Armor=wood Sight=1 Owner=GDI Cost=1000 ;was 1200 Points=30 Power=-20 Crewed=no Capturable=no Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 PowersUpBuilding=gaplug PowersUpToLevel=-1 ThreatPosed=0 ; This value MUST be 0 for all building addons SuperWeapon=DropPodSpecial AIBuildThis=no ; Firestorm defense [GAFIRE] Name=Fire Storm Generator Strength=800 Armor=heavy TechLevel=9 Prerequisite=GATECH Adjacent=2 Sight=5 Owner=GDI Cost=2000 Points=30 Power=-200 Crewed=yes Capturable=true Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=9 SuperWeapon=FirestormSpecial DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=410, 600, 290 ; Laser fence post [NAPOST] Name=Laser Fence Post Prerequisite=NAAPWR Strength=300 Armor=concrete TechLevel=8 Adjacent=3 Sight=4 Owner=Nod Cost=200 BaseNormal=no Points=30 Power=-25 Crewed=no Capturable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 LaserFencePost=yes MaxDebris=2 Powered=yes ThreatPosed=0 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys BaseNormal=no GuardRange=10 ; Used to set max. intra-post distance [NAFNCE] Name=Laser Fence Section Strength=800 Armor=concrete TechLevel=-1 Sight=1 Owner=Nod Capturable=false Cost=0 Points=00 Power=0 Selectable=no Crewed=no LaserFence=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=10 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys BaseNormal=no LegalTarget=false TogglePower=no Repairable=false ;don't allow this to be blown up with C4 ; Laser turret [NALASR] Name=Laser Strength=500 Armor=wood Prerequisite=NAHAND TechLevel=2 Adjacent=4 ROT=10 Sight=7 Owner=Nod Cost=300 BaseNormal=no Points=30 Power=-40 Crewed=no Capturable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=2 Primary=LaserFire2 Turret=yes TurretAnim=LASER TurretAnimIsVoxel=true TurretAnimX=-8 TurretAnimY=16 TurretAnimZAdjust=-40 ThreatPosed=30 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys IsBaseDefense=yes HasStupidGuardMode=false ; SAM [NASAM] Name=Sam Strength=600 Armor=wood TechLevel=5 Prerequisite=NARADR Adjacent=4 Sight=10 Owner=Nod Cost=500 BaseNormal=no Points=30 Power=-30 Crewed=no Primary=RedEye2 Capturable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 ThreatPosed=0 ; This value should be 0 for objects that are purely anti-air, since aircraft do not use the threat values DamageParticleSystems=SparkSys,LGSparkSys IsBaseDefense=yes Powered=yes Turret=yes TurretAnim=NASAM_A TurretAnimIsVoxel=false TurretAnimX=-2 TurretAnimY=10 TurretAnimZAdjust=-20 HasStupidGuardMode=false [GAFSDF] Name=Firestorm Wall Section Strength=200 Armor=concrete Prerequisite=GAFIRE TechLevel=9 Repairable=false Sight=2 Owner=GDI Capturable=false Cost=250 ;50 Points=00 Power=-2 Selectable=no Crewed=no FirestormWall=yes Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ThreatPosed=20 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,LGSparkSys ;IsBase=no ;Crim: not a rules flag BaseNormal=no TogglePower=no Insignificant=yes GuardRange=5 [ABAN01] Name=WS Logging Company TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN02] Name=Pannullo Hacienda TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN03] Name=Abandoned Factory TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN04] Name=City Hall TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN05] Name=Hunting Lodge TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN06] Name=Local Inn & Lodging TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN07] Name=Church TechLevel=-1 Strength=350 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN08] Name=Abandoned Warehouse TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN09] Name=Tall's Residence TechLevel=-1 Strength=350 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN10] Name=Denzil's Last Chance Motel TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN11] Name=Miele Manor TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN12] Name=Kettler's Place TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN13] Name=Long's Home TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN14] Name=Local Store TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN15] Name=Adam's House TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN16] Name=Gas Station TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=2 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN17] Name=Gas Pumps TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=2 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [ABAN18] Name=Gas Station Sign TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=2 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0001] Name=Rade's Roadhouse TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no ;Crim: not a rules flag BaseNormal=no [CA0002] Name=Sandberg and Son's TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0003] Name=Temp Housing TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0004] Name=Waystation TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0005] Name=Ferbie's 4 Sale TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0006] Name=Deluxe Accomodations TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0007] Name=Field Generator TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0008] Name=Subterranean Dwelling TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0009] Name=Subterranean Dwelling TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0010] Name=Leary Traveller Inn TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0011] Name=Water Tank TechLevel=-1 Strength=200 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0012] Name=Greenhouse TechLevel=-1 Strength=100 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0013] Name=Water Purifier TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0014] Name=Observation Tower TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0015] Name=Port-A-Shack TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0016] Name=Port-A-Shack Deluxe TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0017] Name=Energy Transformer TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0018] Name=Solar Panel TechLevel=-1 Strength=200 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0019] Name=Solar Panel TechLevel=-1 Strength=200 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0020] Name=Solar Panel TechLevel=-1 Strength=200 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CA0021] Name=Solar Panel TechLevel=-1 Strength=200 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=light Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [GAOLDCC1] Name=Old Construction Yard TechLevel=-1 Strength=400 Insignificant=yes Capturable=false Repairable=false Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no ;Crim: not a rules flag BaseNormal=no [GAOLDCC2] Name=Old Temple TechLevel=-1 Strength=400 Insignificant=yes Capturable=false Repairable=false Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [GAOLDCC3] Name=Old Weapons Factory TechLevel=-1 Strength=400 Capturable=false Repairable=false Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [GAOLDCC4] Name=Old Refinery TechLevel=-1 Strength=400 Capturable=false Repairable=false Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [GAOLDCC5] Name=Old Advanced Power Plant TechLevel=-1 Strength=400 Capturable=false Repairable=false Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [GAOLDCC6] Name=Old Silos TechLevel=-1 Strength=400 Capturable=false Repairable=false Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no ; Use Ammo to specify the number of times to allow healing. [CAHOSP] Name=Civilian Hospital TechLevel=-1 Strength=800 LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Hospital=yes PipScale=Ammo Ammo=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no ;Crim: not a rules flag BaseNormal=no ; Use Ammo to specify number of time the building can be used to upgrade infantry [CAARMR] Name=Civilian Armory TechLevel=-1 Strength=800 Immune=no LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Armory=yes PipScale=Ammo Ammo=5 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CAPYR01] Name=Pyramid TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ;IsBase=no BaseNormal=no [CAPYR02] Name=Pyramid TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ;IsBase=no BaseNormal=no [CAPYR03] Name=Pyramid TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 ;IsBase=no BaseNormal=no [CACRSH01] Name=Crash 1 TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Selectable=no ;IsBase=no ;Crim: not a rules flag BaseNormal=no [CACRSH02] Name=Crash 2 TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Selectable=no ;IsBase=no BaseNormal=no [CACRSH03] Name=Crash 3 TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Selectable=no ;IsBase=no BaseNormal=no [CACRSH04] Name=Crash 4 TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Selectable=no ;IsBase=no BaseNormal=no [CACRSH05] Name=Crash 5 TechLevel=-1 Strength=400 Immune=yes LegalTarget=no Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Selectable=no ;IsBase=no BaseNormal=no [CAARAY] Name=Civilian Array TechLevel=-1 Strength=400 LegalTarget=yes Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=3 ;IsBase=no BaseNormal=no [GASPOT] Name=Light Tower TechLevel=-1 Strength=400 Points=50 Power=-10 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 HasSpotlight=true MaxDebris=2 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=450, 500, 710 ;IsBase=no BaseNormal=no [CITY01] Name=Connelly Court Apts. TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no ;Crim: not a rules flag BaseNormal=no [CITY02] Name=Lightner's Luxury Suites TechLevel=-1 Strength=700 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY03] Name=Office Building TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY04] Name=Westwood Stock Exchange TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY05] Name=Daily Sun Times TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY06] Name=YEO-CA Cola Corp. TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY07] Name=Urban Housing TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY08] Name=Yee's Discount Liquor TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY09] Name=Abandoned Warehouse TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY10] Name=Urban Storefront TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY11] Name=Ambrose Lounge TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY12] Name=Bostic Tower TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY13] Name=Hewitt Hair Salon TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY14] Name=Business Offices TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY15] Name=2nd National Bank TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY16] Name=Highrise Hotel TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY17] Name=The Projects TechLevel=-1 Strength=300 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY18] Name=Archer Asylum TechLevel=-1 Strength=600 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=concrete Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY19] Name=Fill'er Up-Pump'N'Go TechLevel=-1 Strength=500 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY20] Name=Gas Pump TechLevel=-1 Strength=250 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=wood Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=6 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY21] Name=Gas Station Sign TechLevel=-1 Strength=100 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=none Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CITY22] Name=Church TechLevel=-1 Strength=100 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=none Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no BaseNormal=no [CTVEGA] Name=Vega's Pyramid TechLevel=-1 Strength=100 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=none Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 DamageParticleSystems=SmallGreySSys,BigGreySmokeSys ;IsBase=no ;Crim: not a rules flag BaseNormal=no [BBOARD01] Name=Eat at Rade's Roadhouse TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD02] Name=Drink YEO-CA Cola! TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD03] Name=Hamburgers $.99 TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD04] Name=Visit Scenic Las Vegas TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD05] Name=Rooms $29 a nite TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD06] Name=Kaspm's Tiberium Warhouse TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD07] Name=Alkaline's Battery Superstore TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD08] Name=Alex-gators petshop just ahead! TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD09] Name=TacticX games rock! TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD10] Name=WW Surf and Turf hits the spot! TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD11] Name=Only 11 miles to Zydeko's cafe! TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD12] Name=No escape from Archer's Asylum! TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD13] Name=Stop in at Hewitt's hair salon TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD14] Name=Billy Bob's Harvester school TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD15] Name=Pannullo's hacienda es bueno TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [BBOARD16] Name=Join GDI: We save lives. TechLevel=-1 Strength=400 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=0 Selectable=no ;IsBase=no BaseNormal=no [CTDAM] Name=Dam TechLevel=1 Strength=1000 Power=200 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 PlaceAnywhere=yes ;DamageParticleSystems=SparkSys,LGSparkSys WST 7/16 commented out cuz it looked like bad palette DamageSmokeOffset=500, 100, 500 ;IsBase=no ;Crim: not a rules flag BaseNormal=no [UFO] Name=Scrin Ship TechLevel=-1 Strength=1000 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=8 PlaceAnywhere=yes ;IsBase=no BaseNormal=no ; Kodiak crash [C_KODIAK] Name=Kodiak Crash TechLevel=-1 Strength=1000 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=heavy MaxDebris=0 PlaceAnywhere=yes [AMMOCRAT] Name=Ammo Crates Image=AMMO01 Selectable=no Explodes=yes TechLevel=-1 Strength=1 Insignificant=yes Nominal=yes RadarInvisible=yes Points=5 Armor=none Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 MaxDebris=4 PlaceAnywhere=yes ;IsBase=no BaseNormal=no ThreatPosed=10 SpecialThreatValue=1 ; Core Defender (deployed) [DDEFD] Name=Core Defender Image=DEFD Capturable=false Owner=Civilian UndeploysInto=DEFENDER MaxDebris=10 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Strength=9999 Armor=concrete TechLevel=-1 Adjacent=2 Sight=6 Cost=10000 Points=10000 Power=0 Powered=false Sensors=yes ThreatPosed=0 DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=375,550,500 AIBuildThis=no IsCoreDefender=yes UndeploySound=COREUP1 Immune=yes ; Cabal Core [CORE] Name=Cabal Core TechLevel=-1 Strength=3000 Nominal=yes RadarInvisible=yes Points=5 Armor=concrete MaxDebris=0 Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=-60,60,200 PlaceAnywhere=yes ; NOD AA Obelisk [AAOB] Name=Obelisk of Darkness Image=OBL2 Prerequisite= Strength=1000 Armor=concrete TechLevel=-1 ;9 Adjacent=2 Sight=8 Owner=Civilian Cost=1500 Points=30 Power=0 Crewed=yes Capturable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Primary=AALaserFire Turret=no TurretAnim=OBL2_C TurretAnimZAdjust=-3 TurretChargeAnimRate=1 TurretAnimIsExclusive=yes MaxDebris=4 ThreatPosed=40 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=-60,60,200 IsBaseDefense=yes BaseNormal=no Powered=yes HasStupidGuardMode=false ; CABAL Core Obelisk Ground [CROB] Name=CABAL Obelisk Image=OBL1 Prerequisite= Strength=1000 Armor=concrete TechLevel=-1 Adjacent=2 Sight=8 Owner=Civilian Cost=1500 Points=30 Power=0 Crewed=yes Capturable=false Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60 Primary=CABLaser Turret=no TurretAnim=OBL1_C TurretAnimZAdjust=-100 TurretChargeAnimRate=1 TurretAnimIsExclusive=yes MaxDebris=4 ThreatPosed=40 ; This value MUST be 0 for all building addons DamageParticleSystems=SparkSys,SmallGreySSys,BigGreySmokeSys DamageSmokeOffset=120,0,30 IsBaseDefense=yes BaseNormal=no Powered=yes HasStupidGuardMode=false ; ******* Weapon Statistics ******* ; The weapons specified here are attached to the various combat ; units and buildings. ; Anim = animation to display as a firing effect [use 8 for directional variation] ; Burst = number of rapid succession shots from this weapon (def=1) ; Camera = Reveals area around firer (def=no)? ; Charges = Does it have charge-up-before-firing logic (def=no)? ; Damage = the amount of damage (unattenuated) dealt with every bullet ; Floater = floats like a frizbee ; Lobber = does the projectile fly to target in a high arc (def=no)? ; Projectile = projectile characteristic to use ; ROF = delay between shots [15 = 1 second at middle speed setting] ; Range = maximum cell range ; MinimumRange = minimum range to target (def=0) ; Report = List of sounds to random play when firing ; Speed = speed of projectile to target (100 is maximum) ; Warhead = warhead to attach to projectile ; Supress = Should nearby friendly buildings be scanned for and if found, discourage firing on target (def=no)? ; TurboBoost = Should the weapon get a boosted speed bonus when firing upon aircraft? ; UseFireParticles = Should the weapon spawn a flame particle system? (def = no) ; Bright = Does this weapons bullet cause a lighting effect when it impacts (def=no)? If set, this will override the warheads 'Bright' flag. ; IonSensitive = Shuts down during an ion storm (def=no)? [Weapons] 00=Vulcan2 01=MultiLauncher 02=ChemLauncher 03=VulcanTower 04=EMPulseWeapon 05=LaserFire 06=LaserFire2 07=RedEye2 08=RPGTower 09=90mm 10=120mmx 11=155mm 12=Bomb 13=Proton 14=HarpyClaw 15=Hellfire 16=MammothTusk 17=120mm 18=BikeMissile 19=HoverMissile 20=SonicZap 21=Dragon 22=MechRailgun 23=AssaultCannon 24=RepairBullet 25=FireballLauncher 26=RaiderCannon 27=SlimeAttack 28=SuicideBomb 29=Minigun 30=M1Carbine 31=Grenade 32=BAZOOKA 33=Heal 34=MultiCluster 35=Vulcan 36=JumpCannon 37=FiendShard 38=CyCannon 39=Sniper 40=LtRail 41=Vulcan3 42=Pistola 43=DropGun 44=Jugg90mm 45=LIMP 46=AALaserFire 47=CABLaser 48=QuadLauncher 49=WebLauncher 50=Tentacle 51=DEFOB 52=DualRockets 53=MobileEMPulseWeapon ;Light infantry Rifle [Minigun] Damage=8 ROF=21 Range=4 Projectile=Invisible Speed=100 Warhead=SA Report=INFGUN3,GOSTGUN1,SLVKGUN1 ;Rocket Infantry [BAZOOKA] Damage=25 ROF=60 Range=6 Projectile=AAHeatSeeker2 Speed=25 Warhead=AP Report=RKETINF1 ;Jump Jet Cannon [JumpCannon] Damage=15 ; was 20 Burst=2 ROF=40 Range=5 ; was 4 Projectile=Invisible3 Speed=100 Warhead=SA Report=JUMPJET1 ;Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW ;Assault Suit Cannon [AssaultCannon] Damage=40 ROF=50 Range=5 Projectile=Invisible Speed=100 Warhead=SA Report=TSGUN4 ;Cyborg Commando Plasma Cannon [CyCannon] Damage=120 ROF=50 Range=7 Projectile=ProtonBlast Speed=70 Warhead=PlasmaWH Report=scrin5b ;Harpy Vulcan Cannon [HarpyClaw] Damage=60 ROF=36 Range=5 Projectile=Invisible2 Speed=100 Warhead=SA Report=CYGUN1 ;Assault Buggy Cannon [RaiderCannon] Damage=40 ROF=55 Range=4 Projectile=Invisible Speed=100 Warhead=SA Report=CHAINGN1 Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW ;Vulcan Tower Cannon [VulcanTower] Damage=18 ROF=26 Range=6 Projectile=Invisible Speed=100 Warhead=SA Report=CHAINGN1 Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW Bright=true ;Rocket Tower Grenade [RPGTower] Damage=110 ROF=80 Range=8 Projectile=Lobbed2 Speed=30 ; 5 Warhead=RPG MinimumRange=2 Report=GLNCH4 ;Bike Missile [BikeMissile] Damage=40 ROF=60 Range=5 Projectile=HeatSeeker Speed=30 Report=MISL1 Warhead=AP ;SAM missile [RedEye2] Damage=33 ROF=55 Range=15 Projectile=AAHeatSeeker Speed=30 Warhead=SAMWH Report=SAMSHOT1 TurboBoost=yes ;Hover Missile [HoverMissile] Damage=30 ROF=68 Range=8 Burst=2 Projectile=AAHeatSeeker2 Speed=30 Warhead=AP Report=HOVRMIS1 MinimumRange=2 ;Ghost's Rail Gun [LtRail] Damage=0 ; this should be 0 for railgun shots AmbientDamage=150 ; use this for the railgun damage field. Leave damage = 0 ROF=60 ; ROF for railgun is tied to the duration (MaxEC) of the railgun particle Range=6 Projectile=Invisible Speed=100 Warhead=RailShot2 Anim=GUNFIRE IsRailgun=true AttachedParticleSystem=SmallRailgunSys Report=BIGGGUN1 ;Mammoth Rail Gun [MechRailgun] AmbientDamage=200 ; use this for the railgun damage field. Leave damage = 0 Damage=0 ; this should be 0 for railgun shots ROF=60 ; ROF for railgun is tied to the duration (MaxEC) of the railgun particle Range=8 Projectile=Invisible Speed=100 Warhead=RailShot Report=RAILUSE5 Anim=GUNFIRE IsRailgun=true AttachedParticleSystem=LargeRailgunSys Burst=2 ;Crim: allows both lateral firing offsets ; rapid fire machine gun [Vulcan] Damage=20 ROF=60 Range=4 Projectile=Invisible Speed=100 Warhead=SA Report=CHAINGN1 Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW ;Cyborg's Vulcan cannon [Vulcan3] Damage=10 ROF=30 Burst=3 Range=4 Projectile=Invisible Speed=100 Warhead=SA Report=CYGUN1 ;Anim=MGUN-N,MGUN-NE,MGUN-E,MGUN-SE,MGUN-S,MGUN-SW,MGUN-W,MGUN-NW ; fireball from flame tank [FireballLauncher] Damage=0 AmbientDamage=2 ROF=50 Range=4.25 Projectile=Invisible Speed=1 Warhead=Fire Report=FLAMTNK1 UseFireParticles=yes AttachedParticleSystem=FireStreamSys Burst=2 ; sniper rifle [Sniper] Damage=150 ROF=60 Range=6.75 Projectile=Invisible Speed=100 Warhead=HollowPoint Report=SILENCER ; rifle soldier weapons (multiple shots) [M1Carbine] Damage=15 ROF=20 Range=4 Projectile=Invisible Speed=100 Warhead=SA Report=INFGUN3 ; man-packed anti-tank missile (bazooka type) [Dragon] Damage=30 ROF=50 Burst=2 Range=6 Projectile=AAHeatSeeker2 Speed=25 Warhead=AP Report=MISL1 ; air-to-surface homing missile (launched from helicopter) [Hellfire] Damage=30 ; 25 ROF=50 Range=6 Projectile=AAHeatSeeker2 ; was HeatSeeker Speed=30 Warhead=ORCAAP Report=ORCAMIS1 Burst=2 [Proton] Damage=20 ROF=3 Range=5 Projectile=ProtonTorpedo Speed=30 Warhead=AP Report=scrin5b ; hand grenade (discus) [Grenade] Damage=40 ROF=60 Range=4.5 Projectile=Lobbed ;Floater=yes Speed=5 Warhead=HE ;Lobber=yes Bright=yes ; small anti-armor cannon [75mm] Damage=35 ROF=40 Range=6 Projectile=Cannon Speed=40 Warhead=AP Anim=GUNFIRE Bright=yes ; light anti-armor cannon [90mm] Damage=36 ROF=50 Range=6.75 Projectile=Cannon Speed=40 Warhead=AP Report=120MMF Anim=GUNFIRE Bright=yes ; large anti-armor cannon (two shooter) [120mmx] Damage=50 ROF=80 Range=6.75 Projectile=Cannon Speed=40 Warhead=AP Report=120MMX9 Anim=GUNFIRE Burst=2 Bright=yes ; large anti-armor cannon (single shooter) [120mm] Damage=70 ROF=80 Range=6.75 Projectile=Invisible Speed=90 Warhead=AP Report=120MMF Anim=GUNFIRE Bright=yes [Bomb] Damage=160 ROF=10 ; was 1 Range=5 Projectile=Cannon2 Speed=0 Warhead=ORCAHE Floater=yes [SuicideBomb] Damage=11000 ROF=1 Range=.5 Projectile=Invisible Speed=0 Warhead=Super Bright=yes Report=HUNTER2 ; Vehicle carried anti-tank missile [MammothTusk] Damage=40 ROF=80 Range=6 Projectile=AAHeatSeeker Speed=20 Warhead=HE Burst=2 Report=MISL1 ; artillery cannon [155mm] Damage=115 ;150 ROF=150 ;110 Range=18 MinimumRange=5 Projectile=Ballistic Speed=10 Warhead=ARTYHE Report=120MMF Anim=GUNFIRE Lobber=yes [Jugg90mm] Damage=75 ROF=150 ;was 110 Range=18 MinimumRange=5 Projectile=Ballistic2 Speed=10 Warhead=ARTYHE Report=JUGGER1 Anim=GUNFIRE Lobber=yes BurstDelay0=0 BurstDelay1=24 Burst=3 ; Sonic Zap [SonicZap] Damage=1 AmbientDamage=3 ROF=120 Range=6 Projectile=Null Speed=100 Warhead=SonicWarhead Report=SONIC4 IsSonic=Yes ; repair bot repairing [RepairBullet] Damage=-50 ROF=80 Range=1.8 Projectile=Invisible Speed=100 Warhead=Mechanical Report=REPAIR11 UseSparkParticles=yes AttachedParticleSystem=WeldingSys ; medic healing [Heal] Damage=-50 ROF=80 Range=2.83 Projectile=Invisible Speed=100 Warhead=Organic Report=HEALER1 ; rapid fire machine gun [Vulcan2] Damage=50 ROF=50 Range=6 Projectile=Invisible Speed=100 Warhead=SA Report=TSGUN4 Anim=GUNFIRE ; Drop Pod Gun [DropGun] Damage=50 ROF=50 Range=6 Projectile=Invisible Speed=100 Warhead=SA Report=TSGUN4 Anim=GUNFIRE ; Solid laser beam. [LaserFire] Damage=250 ROF=120 Range=10.5 Speed=100 Warhead=Super Report=OBELRAY1 LaserInnerColor = 255,0,0 LaserOuterColor = 0,0,0 LaserOuterSpread= 20,40,40 LaserDuration = 15 Projectile=LLine IsBigLaser=true IsLaser=true ; this flag tells the game to use the special laser draw effect Charges=yes ; non-charging lasers should use LaserFire2 instead ; Laser Turret Beam [LaserFire2] Damage=30 ROF=40 Range=5.5 Speed=100 Warhead=Super Report=LASTUR1 Charges=no LaserInnerColor = 255,0,0 LaserOuterColor = 0,0,0 LaserOuterSpread= 20,40,40 LaserDuration = 15 Projectile=LLine2 IsLaser=true ; this flag tells the game to use the special laser draw effect ;Core Defender Obelisk [DEFOB] Damage=350 ROF=20 Burst=2 Range=10.5 Speed=100 Warhead=Super2 Report=OBELCOR3 LaserInnerColor = 0,0,255 LaserOuterColor = 0,0,0 LaserOuterSpread= 20,40,40 LaserDuration = 2 ;was 15 Projectile=LLine IsBigLaser=true IsLaser=true ; this flag tells the game to use the special laser draw effect Charges=yes ; non-charging lasers should use LaserFire2 instead ; Solid AA laser beam. [AALaserFire] Damage=250 ROF=20 Range=12 ;was 10.5 Speed=100 Warhead=Super Report=OBELMOD1 LaserInnerColor = 0,0,255 LaserOuterColor = 0,0,0 LaserOuterSpread= 20,40,40 LaserDuration = 15 Projectile=AALLine IsBigLaser=true IsLaser=true ; this flag tells the game to use the special laser draw effect Charges=yes ; non-charging lasers should use LaserFire2 instead ;Advance CABAL Obelisk Laser [CABLaser] Damage=100 ROF=70 Range=10.5 Speed=100 Warhead=Super Report=OBELRAY1 LaserInnerColor = 0,0,255 LaserOuterColor = 0,0,0 LaserOuterSpread= 20,40,40 LaserDuration = 15 Projectile=LLine IsBigLaser=true IsLaser=true ; this flag tells the game to use the special laser draw effect Charges=yes ; non-charging lasers should use LaserFire2 instead [EMPulseWeapon] Damage=1200 ; Damage is duration for EM Pulse ROF=1 Speed=25 Warhead=EMPuls Projectile=PulsPr Range=40 ; was 30 Lobber=yes Report=PLSECAN2 ; Do NOT give this weapon to any units or all hell will break loose. [MobileEMPulseWeapon] Damage=1200 ; Damage is duration for EM Pulse ROF=1 Speed=25 Warhead=MobileEMPulse Projectile=PulsPr Range=40 ; was 30 Lobber=yes Report=PLSECAN2 ; Visceroid attack [SlimeAttack] Damage=100 ROF=80 Range=1.3 ;2.83 Projectile=Invisible Speed=25 ;100 Warhead=Slimer Report=VICER1 ; Chemical missile launcher [ChemLauncher] Damage=100 ROF=1 ; 0 Range=6 Projectile=ChemMissile Speed=30 Warhead=Gas Report=ICBM1 [FiendShard] Damage=35 ROF=30 Burst=3 Range=5 Projectile=DogShard Speed=25 Warhead=Shard Report=FIEND2 [MultiLauncher] Damage=130 ROF=80 Range=30 Projectile=MultiMissile Speed=35 ; was 10 Warhead=HE Report=SAMSHOT1 [Pistola] Damage=2 ROF=20 Range=3 Projectile=Invisible Speed=100 Warhead=SA Report=GUN18 ; MultiMissile Cluster Missiles [MultiCluster] Damage=65 ROF=80 Range=6 Projectile=HeatSeeker Speed=20 Warhead=HE Burst=2 Report=MISL1 ; Limpet Drone Weapon [LIMP] Damage=1 ROF=80 Range=2 Projectile=LimpetBullet Speed=100 Warhead=LIMPY Report=LIMPBOM1 [WebLauncher] Damage=0 ROF=200 ;was 180 Range=7 Projectile=WebCapsule Speed=25 ; was 10 Warhead=WebMass Report=FIREWEB1 AttachedParticleSystem=SmallGreySSys ; Second Stage of Cyborg Spider rocket launcher [DualRockets] Damage=5 ;was 4 ROF=180 ;was 80 Range=6 Projectile=AAHeatSeeker2 Speed=15 Warhead=AP Burst=2 Report=RKETINF1 ; First stage of Cyborg Spider rocket launcher [QuadLauncher] Damage=0 ROF=180 ;was 80 Range=7 ProjectileRange=2 MinimumRange=3 ; was 2 Projectile=DualCluster Speed=25 ; was 10 Warhead=SA Report=SAMSHOT1 Burst=2 ; Jellyfish Tentacle [Tentacle] Damage=16 ROF=80 Range=15 Projectile=Invisible Speed=25 ; Not used; see levitation parameters Warhead=Stinger Report=FLOATK1 ; ******* Projectile Statistics ******* ; Projectiles describe how and what image to use as the weapon flies ; to its target. Think of the projectile as the "delivery method" used ; to get the warhead to the desired target. ; AA = Can this weapon fire upon flying aircraft (def=no)? ; AG = Can this weapon fire upon ground objects (def=yes)? ; ASW = Is this an Anti-Submarine-Warfare projectile (def=no)? ; Acceleration = amount to increase missile speed (def=3) ; Airburst = Does it try to fly over the target instead of hit it (def=no)? ; Arm = arming delay (def=0) ; Bouncy = Does it bounce a bit upon impact (def=no)? ; Degenerates = Does the bullet strength weaken as it travels (def=no)? ; Dropping = Does it fall from a starting height (def=no)? ; Elasticity = "bounciness" of the object [should be 0.0 through 1.0] (def=0.75) ; High = Can it fly over walls (def=no)? ; Image = image to use during flight ; Inaccurate = Is it inherently inaccurate (def=no)? ; Inviso = Is the projectile invisible as it travels (def=no)? ; Parachuted = Equipped with a parachute for dropping from plane (def=no)? ; Proximity = Does it blow up when near its target (def=no)? ; ROT = Rate Of Turn [non zero implies homing] (def=0) ; Ranged = Can it run out of fuel (def=no)? ; Shadow = If High, does this bullet need to have a shadow drawn? (def = yes) ; Color = Color scheme to use for special remapping projectiles (def = none) ; VeryHigh = Does it fly at a very high cruise altitude (def=no)? ; Cluster = number of explosions attached to projectile [cluster-bomb] (def=1) ; invisible flight to target [Invisible] Inviso=yes Image=none ; Harpy Claw tracking projectile [Invisible2] Inviso=yes Image=none ROT=3 AA=yes AG=yes ; Jump jet cannon [Invisible3] Inviso=yes Image=none AA=yes AG=yes [Null] Inviso=yes Arm=9999999 Image=none ; straight high-speed ballistic shot [Cannon] Image=120MM Arcing=true ; orca bomber bomblets [Cannon2] Image=120MM AA=no ; chemical missile [ChemMissile] Arm=2 High=yes VeryHigh=yes Cluster=8 ;Shadow=no Proximity=yes Ranged=yes AA=no Image=MISLCHEM ROT=4 Color=DarkGreen IgnoresFirestorm=yes [MultiMissile] Arm=2 High=yes VeryHigh=yes ;Shadow=no Proximity=yes Cluster=10 ; number of small missiles to launch Ranged=yes AA=no Image=MISLMLTI ROT=4 Color=DarkGreen Airburst=yes AirburstWeapon=MultiCluster IgnoresFirestorm=yes ; small homing missile (targets vehicles best) [HeatSeeker] Arm=2 High=yes Shadow=no Proximity=yes Ranged=yes Image=DRAGON ROT=8 [ProtonTorpedo] Arm=2 High=yes Shadow=no Proximity=yes Ranged=yes Image=TORPEDO ROT=1 IgnoresFirestorm=yes [ProtonBlast] High=yes Shadow=no Proximity=yes Ranged=yes Image=TORPEDO ROT=1 IgnoresFirestorm=yes ; aircraft-only heatseeker [AAHeatSeeker] Arm=2 High=yes Shadow=no Proximity=yes Ranged=yes AA=yes AG=no Image=DRAGON ROT=5 ; aircraft and ground heatseeker [AAHeatSeeker2] Arm=2 High=yes Shadow=no Proximity=yes Ranged=yes AA=yes AG=yes Image=DRAGON ROT=8 [Lobbed] High=yes Image=DISCUS Bouncy=yes Arcing=yes Floater=yes [Lobbed2] High=yes Image=CANISTER Arcing=true ; arcing ballistic projectile [Ballistic] High=yes Image=120MM Arcing=true ; arcing ballistic projectile [Ballistic2] High=yes Image=120MM Arcing=true Inaccurate=true Bouncy=yes Elasticity=0.0 [PulsPr] High=yes Image=PULSBALL [LLine] Inviso=yes Image=none AA=no AG=yes [LLine2] Inviso=yes Image=none AA=no AG=yes [AALLine] Inviso=yes Image=none AA=yes AG=no [DogShard] Image=CRYSTAL4 Arcing=true [WebCapsule] Arm=2 High=no Shadow=no Proximity=yes Ranged=yes AA=no AG=yes Image=WEB ROT=5 ; Cyborg Spider multi-missile [DualCluster] High=no VeryHigh=no ;Shadow=no Proximity=no Cluster=2 ; number of small missiles to launch Ranged=yes Range=3 AA=yes Image=DRAGON ROT=4 Color=DarkGreen Splits=yes AirburstWeapon=DualRockets IgnoresFirestorm=yes RetargetAccuracy=75% ; Limpet Projectile [LimpetBullet] Inviso=yes Image=none AV=true ; *** Particle Systems *** ; This is a list of the various types of particles systems available in the game [ParticleSystems] 1=GasCloudSys 2=FireStreamSys 3=BigGreySmokeSys 4=SmallGreySSys 5=DebrisSmokeSys 6=SparkSys 7=FirestormSparkSys 8=TestSmokeSys 9=SmallRailgunSys 10=LargeRailgunSys 11=WeldingSys 12=LGSparkSys 13=WebSys 14=GasPuffSys 15=SmokeStackSys ; HoldsWhat = type of particle (see below) that this system manages (required) ; Spawns = does this system spawn particles by itself (def = no) ; SpawnFrames = number of frames to wait before spawning another particle ; ParticleCap = maximum number of particles that can be in this system [SmallRailgunSys] HoldsWhat=SmallRailgunPart BehavesLike=Railgun SpiralRadius=6 ParticlesPerCoord=.1 SpiralDeltaPerCoord=.035 MovementPerturbationCoefficient=.3 PositionPerturbationCoefficient=20 VelocityPerturbationCoefficient=.6 Laser=yes LaserColor=255,128,0 [LargeRailgunSys] HoldsWhat=LargeRailgunPart BehavesLike=Railgun SpiralRadius=15 ParticlesPerCoord=.15 SpiralDeltaPerCoord=.03 MovementPerturbationCoefficient=.4 PositionPerturbationCoefficient=30 VelocityPerturbationCoefficient=.6 Laser=yes LaserColor=25,20,255 ;R,G,B for laser color [WeldingSys] HoldsWhat=WeldingSpark BehavesLike=Spark ParticleCap=25 SparkSpawnFrames=20 LightSize=25 OneFrameLight=true SpawnSparkPercentage=.4 [SparkSys] HoldsWhat=Spark BehavesLike=Spark ParticleCap=12 SparkSpawnFrames=1 LightSize=21 SpawnSparkPercentage=1 [FirestormSparkSys] HoldsWhat=FirestormSpark BehavesLike=Spark ParticleCap=25 SparkSpawnFrames=1 LightSize=21 SpawnSparkPercentage=1 ; this is the global gas system [GasCloudSys] HoldsWhat=GasCloud1 BehavesLike=Gas ; a system for large amounts of smoke (damaged buildings, destroyed things) [BigGreySmokeSys] HoldsWhat=LargeGreySmoke Spawns=yes SpawnFrames=10 SpawnRadius=10 Slowdown=.0025 ParticleCap=30 SpawnCutoff=15.0 SpawnTranslucencyCutoff=13.0 BehavesLike=Smoke ; a system for small amounts of smoke (damaged units) [SmallGreySSys] HoldsWhat=SmallGreySmoke Spawns=yes SpawnFrames=5 SpawnRadius=5 Slowdown=.0025 ParticleCap=15 SpawnCutoff=13.0 SpawnTranslucencyCutoff=12.5 BehavesLike=Smoke ; a system for small amounts of smoke (damaged units) [TestSmokeSys] HoldsWhat=TestSmoke Spawns=yes SpawnFrames=10 SpawnRadius=5 Slowdown=.0025 ParticleCap=15 SpawnCutoff=13.0 SpawnTranslucencyCutoff=12.5 BehavesLike=Smoke [DebrisSmokeSys] HoldsWhat=SmallGreySmoke Spawns=yes SpawnFrames=2 SpawnRadius=3 ParticleCap=15 SpawnCutoff=13.0 SpawnTranslucencyCutoff=13.0 BehavesLike=Smoke [FireStreamSys] HoldsWhat=FireStream Spawns=yes SpawnFrames=4 BehavesLike=Fire Image=TWLT036 Lifetime=30 ; was 100 [LGSparkSys] HoldsWhat=LargeSpark BehavesLike=Spark ParticleCap=15 SparkSpawnFrames=5 LightSize=25 OneFrameLight=true SpawnSparkPercentage=.2 [WebSys] HoldsWhat=Web BehavesLike=Web ParticleCap=20 SpawnRadius=10 Lifetime=30 Spawns=no SpawnFrames=10 Slowdown=0.05 SpawnCutoff=15.0 SpawnTranslucencyCutoff=13.0 [GasPuffSys] HoldsWhat=WeakGasCloud BehavesLike=WeakGas Lifetime=3 ; 30 [SmokeStackSys] HoldsWhat=SmokeStackPuff Spawns=yes SpawnFrames=2 SpawnRadius=3 ParticleCap=15 SpawnCutoff=13.0 SpawnTranslucencyCutoff=13.0 BehavesLike=Smoke Lifetime=75 ; *** Particles *** ; This is a list of the various particle types in the game ; These are usually objects of gassy nature: poison gas, smoke, fire, etc... [Particles] ; These first three must be in this order! 1=GasCloud1 2=GasCloud2 3=FireStream 4=Spark 5=FirestormSpark 6=LargeGreySmoke 7=SmallGreySmoke 8=TestSmoke 9=GasCloudD1 10=GasCloudD2 11=SmallRailgunPart 12=LargeRailgunPart 13=GasCloudM1 14=GasCloudM2 15=WeldingSpark 16=LargeSpark 17=Web 18=WeakGasCloud 19=WeakGasCloudD 20=SmokeStackPuff 21=WeakGasCloudM2 ; Image = Imagelist to use for particle ; Persistent = Does this particle stick around; should always be yes ; MaxDC = How many frames go by before this particle damages the things near it? (def = 0) ; MaxEC = How many frames does this object last (def = 0) ; Damage = How much damage does it do (def = 0) ; Warhead = What warhead to use for damage purposes (def = WARHEAD_NONE) ; StartFrame = what frame of image to start on? (def = 0) ; NumLoopFrames = how many frames form a single loop? (def = 1) ; WindEffect = to what degree does the wind affect his particle, 0 = not at all, 5 = a lot (def = 0) ; Velocity = speed at which particle travels (def = 0.0) ; Radius = how big is this particle? (used for attract/repel) ; ; BehavesLike, DeleteOnStateLimit, EndStateAI, StateAIAdvance are things that ; shouldn't be messed with ; the particle that makes up the fire stream of flamethrowers, and flame tanks [FireStream] Image=FLAMEALL Deacc=0.01 Velocity=28.0 BehavesLike=Fire MaxEC=500 MaxDC=3 Warhead=Fire Damage=2 StartStateAI=1 EndStateAI=19 StateAIAdvance=6 Translucent50State=15 Translucent25State=10 DeleteOnStateLimit=yes Normalized=yes FinalDamageState=14 Report=FLAMTNK1 [WeldingSpark] BehavesLike=Spark MaxEC=500 XVelocity=16 YVelocity=16 MinZVelocity=40 ZVelocityRange=15 ColorList=(0,128,255),(255,255,255),(200,200,150),(80,80,80),(0,0,0) StartColor1=80,255,255 StartColor2=255,255,100 ColorSpeed=.13 [Spark] BehavesLike=Spark MaxEC=500 XVelocity=10 YVelocity=10 MinZVelocity=40 ZVelocityRange=15 ColorList=(255,255,255),(200,200,80),(200,10,10),(0,0,0) ColorSpeed=.13 [FirestormSpark] BehavesLike=Spark MaxEC=500 XVelocity=16 YVelocity=16 MinZVelocity=40 ZVelocityRange=15 ColorList=(0,0,255),(255,255,255),(200,200,80),(200,10,10),(0,0,0) ColorSpeed=.13 ; Cloud of Poison Gas #1 Formation particle [GasCloudM1] Image=gaslrgmk MaxDC=60 MaxEC=448 Damage=0 Warhead=Gas StartFrame=0 EndStateAI=11 Translucency=50 WindEffect=0 BehavesLike=Gas StateAIAdvance=3 NextParticle=GasCloud1 DeleteOnStateLimit=yes NextParticleOffset=0,0,150 ; Cloud of Poison Gas #2 formation particle [GasCloudM2] Image=gaslrgmk MaxDC=60 MaxEC=448 Damage=0 Warhead=Gas StartFrame=0 EndStateAI=11 Translucency=50 WindEffect=0 BehavesLike=Gas StateAIAdvance=3 NextParticle=GasCloud2 DeleteOnStateLimit=yes NextParticleOffset=0,0,150 ; Cloud of Poison Gas #1 [GasCloud1] Image=CLOUD1 MaxDC=60 MaxEC=1000 Damage=50 Warhead=Gas StartFrame=0 EndStateAI=28 Translucency=50 WindEffect=0 BehavesLike=Gas StateAIAdvance=4 NextParticle=GasCloudD1 ; Cloud of Poison Gas #2 [GasCloud2] Image=CLOUD2 MaxDC=60 MaxEC=1000 Damage=40 Warhead=Gas StartFrame=0 EndStateAI=28 Translucency=50 WindEffect=0 BehavesLike=Gas StateAIAdvance=4 NextParticle=GasCloudD2 ; Cloud of Poison Gas #1 Dissipation particle [GasCloudD1] Image=CLOUD1D MaxDC=60 MaxEC=50 Damage=10 Warhead=Gas StartFrame=0 EndStateAI=12 Translucency=50 WindEffect=0 BehavesLike=Gas StateAIAdvance=4 DeleteOnStateLimit=yes ; Cloud of Poison Gas #2 dissipating [GasCloudD2] Image=CLOUD2D MaxDC=60 MaxEC=50 Damage=10 Warhead=Gas StartFrame=0 EndStateAI=12 Translucency=50 WindEffect=0 BehavesLike=Gas StateAIAdvance=4 DeleteOnStateLimit=yes [LargeGreySmoke] Image=LGRYSMK1 MaxEC=80 Translucency=25 Velocity=8.0 Deacc=.05 WindEffect=0 BehavesLike=Smoke DeleteOnStateLimit=yes EndStateAI=20 StateAIAdvance=4 [SmallGreySmoke] Image=SGRYSMK1 MaxEC=80 Translucency=25 Velocity=9.0 Deacc=.05 WindEffect=0 BehavesLike=Smoke DeleteOnStateLimit=yes EndStateAI=20 StateAIAdvance=4 [TestSmoke] Image=SGRYSMK1 MaxEC=80 Translucency=25 Velocity=6.0 Deacc=.05 WindEffect=0 BehavesLike=Smoke DeleteOnStateLimit=yes EndStateAI=20 StateAIAdvance=3 [SmallRailgunPart] BehavesLike=Railgun MaxEC=70 ColorList=(200,200,200),(150,150,150) ColorSpeed=.03 Velocity=.4 [LargeRailgunPart] BehavesLike=Railgun MaxEC=70 ColorList=(25,70,205),(150,150,150) ColorSpeed=.009 Velocity=.3 [LargeSpark] BehavesLike=Spark MaxEC=500 XVelocity=13 YVelocity=13 MinZVelocity=40 ZVelocityRange=15 ColorList=(255,255,255),(200,200,80),(200,10,10),(0,0,0) ColorSpeed=.13 [Web] Image=Web BehavesLike=Web Persistent=true MaxDC=2 MaxEC=80 Damage=0 Warhead=none Translucency=25 DeleteOnStateLimit=yes Velocity=8.0 Deacc=.05 WindEffect=0 EndStateAI=10 StateAIAdvance=2 [WeakGasCloud] Image=CLOUD2 MaxDC=60 MaxEC=50 ;1000 Velocity=8.0 Damage=4 Warhead=Gas StartFrame=0 EndStateAI=28 Translucency=50 WindEffect=0 BehavesLike=WeakGas StateAIAdvance=4 NextParticle=WeakGasCloudD [WeakGasCloudD] Image=CLOUD2D MaxDC=60 MaxEC=10 ; 50 Velocity=8.0 Damage=1 Warhead=Gas StartFrame=0 EndStateAI=12 Translucency=50 WindEffect=0 BehavesLike=WeakGas StateAIAdvance=4 DeleteOnStateLimit=yes [SmokeStackPuff] Image=SGRYSMK1 MaxEC=80 Translucency=25 Velocity=9.0 Deacc=.05 WindEffect=0 BehavesLike=Smoke DeleteOnStateLimit=yes EndStateAI=20 StateAIAdvance=4 ; ******* Warhead Characteristics ******* ; This is a list of the various types of warheads available in the game [Warheads] 1=EMPuls 2=SonicWarhead 3=TankOGas 4=SA 5=HE 6=AP 7=Gas 8=Fire 9=HollowPoint 10=Super 11=Organic 12=Slimer 13=FirestormWH 14=IonCannonWH 15=RailShot 16=Mechanical 17=VeinholeWH 18=IonWH 19=ARTYHE 20=PlasmaWH 21=SAMWH 22=ORCAAP 23=RailShot2 24=ORCAHE 25=WebMass 26=LIMPY 27=CoreDefPlasmaWH ; This is what gives the "rock, paper, scissors" character to the game. ; It describes how the damage is to be applied to the target. The ; values should take into consideration the 'area of effect'. ; example: Although an armor piercing tank round would instantly ; kill a soldier IF it hit, the anti-infantry rating is still ; very low because the tank round has such a limited area of ; effect, lacks pinpoint accuracy, and acknowledges the fact that ; tanks pose little threat to infantry that take cover. ; Spread = damage spread factor [larger means greater spread] (def=1) ; [A value of 1 means the damage is halved every pixel distant from center point. ; a value of 2 means damage is halved every 2 pixels, etc.] ; Wall = Does this warhead damage concrete walls (def=no)? ; Wood = Does this warhead damage wood walls (def=no)? ; Fire = Does this produce great heat and thus will melt ice (def=no)? ; Tiberium = Does this warhead destroy tiberium (def=no)? ; Sparky = Does this warhead cause residual flames (def=no)? ; Conventional = Is explosive warhead big enough to cause a splash when it hits water [nukes are too big for this] (def=no)? ; Rocker = Can this warhead cause nearby units to rock upon impact (def=no)? ; AnimList = list of animations to play when warhead explodes [listed from lesser to greater damage] ; Verses = damage value verses various armor types (as percentage of full damage)... ; -vs- none, wood (buildings), light armor, heavy armor, concrete ; InfDeath = which infantry death animation to use (def=0) ; 0=instant die, 1=twirl die, 2=explodes, 3=flying death, 4=burn death, 5=electro ; Deform = % chance that this warhead will damage the ground when it hits. (def=0) ; DeformThreshhold = damage must exceed this amount before deformation can occur (def=0) ; Particle = Particle effect to use when explosion occurs (def=none) ; ProneDamage = Damage modifer for infantry when prone (def=1.0) ; Bright = Does this warhead normally cause a lighting effect when it goes off (def=no). This is overridden in the case of bullets by the weapon 'Bright' flag. ; EM Pulse cannon warhead. [EMPuls] Spread=11 ; Spread is radius of EM pulse effect. EMEffect=yes AnimList=PULSEFX1,PULSEFX2 [MobileEMPulse] Spread=6 ;was 8 EMEffect=yes AnimList=MEMPFX ; PULSEFX1,PULSEFX2 ; warhead for the sonic zap [SonicWarhead] Spread=2 Wood=yes Verses=100%,100%,100%,80%,60% ; was 65, 50 InfDeath=3 Rocker=yes ProneDamage=50% ; warhead for the flying tank of gas [TankOGas] Spread=8 Wall=yes Wood=yes Tiberium=yes Sparky=yes Rocker=no AnimList=TWLT026,TWLT036,TWLT050,TWLT070,TWLT100 Verses=90%,100%,60%,25%,10% Fire=yes InfDeath=4 ProneDamage=50% [RailShot] Spread=1 Verses=200%,175%,160%,100%,25% Rocker=no ProneDamage=100% InfDeath=2 ; Ghost's Railgun [RailShot2] Spread=1 Verses=100%,130%,150%,110%,5% Rocker=no ProneDamage=100% InfDeath=2 ; general multiple small arms fire [SA] Spread=3 Verses=100%,60%,40%,25%,10% InfDeath=1 AnimList=PIFFPIFF,PIFFPIFF Bright=yes ProneDamage=70% ; high explosive (shrapnel) [HE] Spread=4 Wall=yes Wood=yes Verses=100%,85%,70%,35%,28% ; changed conc from 10% Conventional=yes Rocker=no InfDeath=2 AnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG,TWLT070 Deform=10% DeformThreshhold=300 Tiberium=yes Sparky=yes Bright=yes ProneDamage=70% ; Presumes air burst ; Artillery shell [ARTYHE] Spread=6 Wall=yes Wood=yes Verses=40%,85%,68%,35%,35% Conventional=yes Rocker=yes InfDeath=2 AnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG Deform=15% DeformThreshhold=120 Tiberium=yes Bright=yes ProneDamage=30% ;150% ; Ion storm strike [IonWH] Spread=6 Wall=yes Wood=yes Verses=90%,75%,60%,25%,100% Conventional=yes Rocker=yes InfDeath=5 AnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG Deform=10% DeformThreshhold=300 Tiberium=yes Sparky=yes Bright=yes ProneDamage=75% ; Presumes air burst ; armor piercing (discarding sabot, narrow effect) [AP] Spread=3 Wall=yes Wood=yes Verses=25%,65%,75%,100%,60% Conventional=yes InfDeath=3 AnimList=S_CLSN16,S_CLSN22,S_CLSN30,S_CLSN42,S_CLSN58 ProneDamage=50% ; RPG Warhead [RPG] Spread=3 Wall=yes Wood=yes Rocker=yes Verses=30%,75%,90%,100%,70% Conventional=yes InfDeath=3 AnimList=S_CLSN16,S_CLSN22,S_CLSN30,S_CLSN42,S_CLSN58 ProneDamage=100% ; Poison Gas Cloud [Gas] Spread=512 Verses=200%,150%,100%,20%,0% InfDeath=1 Particle=GasCloudSys ProneDamage=300% ; Gas concentrates at gound level [WeakGas] Spread=512 Verses=100%,0%,0%,0%,0% InfDeath=1 ;Particle=WeakGasCloudSys ProneDamage=300% ; Gas concentrates at ground level ; napalm and fire in general [Fire] Spread=8 Wood=yes Verses=600%,148%,59%,6%,2% InfDeath=4 Sparky=yes Fire=yes ProneDamage=600% ; napalm and fire in general, that doesn't set other things on fire (weird, but necessary) [Fire2] Spread=8 Wood=yes Verses=600%,148%,59%,6%,2% InfDeath=4 Sparky=no Fire=yes ProneDamage=600% ; anti-infantry rifle bullet (single shot -- very effective verses infantry) [HollowPoint] Spread=1 Verses=100%,5%,5%,5%,5% InfDeath=1 AnimList=PIFF ProneDamage=100% ; special case damage effect (do not use for regular weapons) [Super] Spread=0 Verses=100%,100%,100%,100%,100% InfDeath=5 Tiberium=yes ProneDamage=60% Sparky=yes [Super2] Spread=0 Verses=100%,100%,100%,100%,100% InfDeath=5 Tiberium=yes ProneDamage=60% Sparky=yes Wall=yes ; special case to only affect mechanical units [Mechanical] Spread=0 Verses=0%,100%,100%,100%,100% InfDeath=0 ; special case to only affect infantry (do not use for regular weapons) [Organic] Spread=0 Verses=100%,0%,0%,0%,0% InfDeath=0 [Slimer] Spread=0 Verses=100%,100%,60%,40%,20% InfDeath=1 ;Crim: was 0, causing the infantry to just disappear in thin air. Slime shouldn't do that. [Shard] Spread=0 Verses=100%,100%,60%,40%,20% InfDeath=1 [FirestormWH] Spread=0 Verses=100%,100%,100%,100%,100% InfDeath=4 [IonCannonWH] Spread=40 Verses=100%,100%,100%,100%,100% InfDeath=5 Wood=yes Wall=yes Fire=yes Deform=100% Sparky=yes [VeinholeWH] Spread=1 Verses=100%,100%,100%,100%,100% InfDeath=0 Veinhole=yes [PlasmaWH] Spread=0 Verses=350%,260%,205%,150%,80% InfDeath=5 Wall=yes Bright=yes Tiberium=yes ProneDamage=350% AnimList=EXPLOMED,EXPLOLRG Sparky=yes [SAMWH] Spread=3 Verses=100%,100%,100%,100%,100% InfDeath=3 AnimList=XGRYSML1,XGRYSML2,EXPLOSML ProneDamage=100% ; Special Orca AP missile [ORCAAP] Spread=2 Wall=yes Wood=yes Verses=30%,65%,150%,100%,30% Conventional=yes InfDeath=3 AnimList=S_CLSN16,S_CLSN22,S_CLSN30,S_CLSN42,S_CLSN58 ProneDamage=50% ; Orca bomber HE bomb [ORCAHE] Spread=512 Wall=yes Sparky=yes Wood=yes Bright=yes Fire=yes Verses=200%,90%,75%,32%,100% ; changed conc from 10% Conventional=yes Rocker=yes InfDeath=2 AnimList=EXPLOMED,EXPLOLRG Deform=8% DeformThreshhold=160 Tiberium=yes ProneDamage=150% ; Limpet Warhead [LIMPY] LimpetFactor=35 Spread=0 Verses=0%,100%,100%,100%,100% InfDeath=0 [WebMass] Wood=no Verses=600%,0%,0%,0%,0% InfDeath=4 Sparky=no Fire=no ProneDamage=100% AnimList=XGRYSML1,XGRYSML2,EXPLOSML,XGRYMED1,XGRYMED2,EXPLOMED,EXPLOLRG,TWLT070 Webby=true WebDuration=600 ;was 300 WebDurationVariation=25 WebRadius=2 Particle=WebSys Spread=4 [Stinger] Spread=0 Verses=60%,45%,90%,55%,0% InfDeath=5 AnimList=PULSEFX1,PULSEFX2 Sparky=yes Bright=yes ProneDamage=45% ; *** Terrain Objects *** ; This is the list of terrain objects. Typically, these include ; trees and rocks. [TerrainTypes] ;1=MINE 2=BOXES01 3=BOXES02 4=BOXES03 5=BOXES04 6=BOXES05 7=BOXES06 8=BOXES07 9=BOXES08 10=BOXES09 11=ICE01 12=ICE02 13=ICE03 14=ICE04 15=ICE05 16=TREE01 17=TREE02 18=TREE03 19=TREE04 20=TREE05 21=TREE06 22=TREE07 23=TREE08 24=TREE09 25=TREE10 26=TREE11 27=TREE12 28=TREE13 29=TREE14 30=TREE15 31=TREE16 32=TREE17 33=TREE18 34=TREE19 35=TREE20 36=TREE21 37=TREE22 38=TREE23 39=TREE24 40=TREE25 41=TIBTRE01 42=TIBTRE02 43=TIBTRE03 44=VEINTREE 45=FONA01 46=FONA02 47=FONA03 48=FONA04 49=FONA05 50=FONA06 51=FONA07 52=FONA08 53=FONA09 54=FONA10 55=FONA11 56=FONA12 57=FONA13 58=FONA14 59=FONA15 60=BIGBLUE3 ; This section lists all the terrain object types and ; specifies their characteristics. ; Immune = Is the terrain immune to combat damage (def=no)? ; WaterBound = Is the terrain only allowed on the water (def=no)? ; SpawnsTiberium = Does it spawn growth of Tiberium around it (def=no)? ; IsFlammable = Can "Forest Fires" spread to and damage this terrain type? [BOXES01] Name=Boxes Immune=yes [BOXES02] Name=Boxes Immune=yes [BOXES03] Name=Boxes Immune=yes [BOXES04] Name=Boxes Immune=yes [BOXES05] Name=Boxes Immune=yes [BOXES06] Name=Boxes Immune=yes [BOXES07] Name=Boxes Immune=yes [BOXES08] Name=Boxes Immune=yes [BOXES09] Name=Boxes Immune=yes [ICE01] Name=Ice Floe Immune=yes WaterBound=yes [ICE02] Name=Ice Floe Immune=yes WaterBound=yes [ICE03] Name=Ice Floe Immune=yes WaterBound=yes [ICE04] Name=Ice Floe Immune=yes WaterBound=yes [ICE05] Name=Ice Floe Immune=yes WaterBound=yes [TREE01] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=6 [TREE02] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE03] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=6 [TREE04] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 [TREE05] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=5 SnowOccupationBits=7 [TREE06] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE07] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE08] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=4 [TREE09] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=4 [TREE10] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=6 SnowOccupationBits=3 [TREE11] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=3 [TREE12] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE13] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=4 [TREE14] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=4 [TREE15] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE16] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE17] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE18] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TREE19] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=2 [TREE20] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=3 [TREE21] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=1 [TREE22] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=3 [TREE23] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=3 [TREE24] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=6 [TREE25] Name=Tree IsFlammable=yes RadarColor=0,192,0 TemperateOccupationBits=4 SnowOccupationBits=7 [TIBTRE01] Name=Tiberium Tree SpawnsTiberium=yes RadarColor=192,192,0 IsAnimated=yes LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=1.5 LightBlueTint=0.01 AnimationRate=3 AnimationProbability=.003 Immune=yes [TIBTRE02] Name=Tiberium Tree SpawnsTiberium=yes RadarColor=192,192,0 IsAnimated=yes LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=1.5 LightBlueTint=0.01 AnimationRate=3 AnimationProbability=.003 Immune=yes [TIBTRE03] Name=Tiberium Tree SpawnsTiberium=yes RadarColor=192,192,0 IsAnimated=yes LightVisibility=4000 LightIntensity=0.01 LightRedTint=0.01 LightGreenTint=1.5 LightBlueTint=0.01 AnimationRate=3 AnimationProbability=.003 Immune=yes [BIGBLUE3] Name=Blue Tiberium Tree SpawnsTiberium=yes TiberiumToSpawn=2 RadarColor=192,192,0 IsAnimated=yes LightVisibility=4000 LightIntensity=0.01 LightRedTint=1.00 LightGreenTint=1.00 LightBlueTint=1.00 AnimationRate=6 AnimationProbability=.003 Immune=yes [VEINTREE] Name=Veinhole Tree Image=None Armor=None IsVeinhole=true Strength=1000 [FONA01] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=7 YDrawFudge=-12 [FONA02] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA03] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA04] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA05] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA06] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA07] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA08] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA09] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA10] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA11] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA12] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA13] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA14] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 [FONA15] Name=Fona IsFlammable=no RadarColor=0,192,0 TemperateOccupationBits=7 SnowOccupationBits=6 YDrawFudge=-12 ; *** Overlay Objects *** ; These specify the various overlay types. Overlays can affect the ; game state (unlike smudges). ; Limits 000-254. 255 is reserved for no overlay [OverlayTypes] 000=GASAND 001=CYCL 002=GAWALL 003=BARB 004=WOOD 005=DUMMY 006=DUMMY2 007=DUMMY3 008=DUMMY4 009=DUMMY5 010=DUMMY6 011=DUMMY7 012=DUMMY8 013=DUMMY9 014=DUMMY10 015=DUMMY11 016=DUMMY12 017=V16 018=V17 019=V18 020=DUMMY13 021=DUMMY14 022=FENC 023=DUMMY15 024=BRIDGE1 ;BRIDGE1 & 025=BRIDGE2 ;BRIDGE2 are the same art. 026=NAWALL 027=BTIB01 028=BTIB02 029=BTIB03 030=BTIB04 031=BTIB05 032=BTIB06 033=BTIB07 034=BTIB08 035=BTIB09 036=BTIB10 037=BTIB11 038=BTIB12 039=TRACKS01 040=TRACKS02 041=TRACKS03 042=TRACKS04 043=TRACKS05 044=TRACKS06 045=TRACKS07 046=TRACKS08 047=TRACKS09 048=TRACKS10 049=TRACKS11 050=TRACKS12 051=TRACKS13 052=TRACKS14 053=TRACKS15 054=TRACKS16 055=TRACKTUNNEL01 056=TRACKTUNNEL02 057=TRACKTUNNEL03 058=TRACKTUNNEL04 059=RAILBRDG1 060=RAILBRDG2 061=CRAT01 062=CRAT02 063=CRAT03 064=CRAT04 065=CRAT0A 066=CRAT0B 067=CRAT0C 068=DRUM01 069=DRUM02 070=PALET01 071=PALET02 072=PALET03 073=PALET04 074=LOBRDG01 075=LOBRDG02 076=LOBRDG03 077=LOBRDG04 078=LOBRDG05 079=LOBRDG06 080=LOBRDG07 081=LOBRDG08 082=LOBRDG09 083=LOBRDG10 084=LOBRDG11 085=LOBRDG12 086=LOBRDG13 087=LOBRDG14 088=LOBRDG15 089=LOBRDG16 090=LOBRDG17 091=LOBRDG18 092=LOBRDG19 093=LOBRDG20 094=LOBRDG21 095=LOBRDG22 096=LOBRDG23 097=LOBRDG24 098=LOBRDG25 099=LOBRDG26 100=LOBRDG27 101=LOBRDG28 102=TIB01 103=TIB02 104=TIB03 105=TIB04 106=TIB05 107=TIB06 108=TIB07 109=TIB08 110=TIB09 111=TIB10 112=TIB11 113=TIB12 114=TIB13 115=TIB14 116=TIB15 117=TIB16 118=TIB17 119=TIB18 120=TIB19 121=TIB20 122=LOBRDGE1 123=LOBRDGE2 124=LOBRDGE3 125=LOBRDGE4 126=VEINS 127=TIB2_01 128=TIB2_02 129=TIB2_03 130=TIB2_04 131=TIB2_05 132=TIB2_06 133=TIB2_07 134=TIB2_08 135=TIB2_09 136=TIB2_10 137=TIB2_11 138=TIB2_12 139=TIB2_13 140=TIB2_14 141=TIB2_15 142=TIB2_16 143=TIB2_17 144=TIB2_18 145=TIB2_19 146=TIB2_20 147=TIB3_01 148=TIB3_02 149=TIB3_03 150=TIB3_04 151=TIB3_05 152=TIB3_06 153=TIB3_07 154=TIB3_08 155=TIB3_09 156=TIB3_10 157=TIB3_11 158=TIB3_12 159=TIB3_13 160=TIB3_14 161=TIB3_15 162=TIB3_16 163=TIB3_17 164=TIB3_18 165=TIB3_19 166=TIB3_20 167=VEINHOLE 168=SROCK01 169=SROCK02 170=SROCK03 171=SROCK04 172=SROCK05 173=TROCK01 174=TROCK02 175=TROCK03 176=TROCK04 177=TROCK05 178=VEINHOLEDUMMY 179=CRATE ; These are graphic objects that can have an effect on the game. ; Tiberium = Is this tiberium [Tiberium grown and graphic logic applies] (def=no)? ; Crate = Is this overlay a crate (def=no)? ; CrateTrigger = Is crate to trigger game events (def=no)? ; RadarInvisible = Is this overlay not visible on the radar map (def=no)? ; Explodes = Does it explode violently when destroyed [i.e., does it do collateral damage] (def=no)? ; LegalTarget = Can it be a legal target for attack (def=no)? ; ChainReaction = Does it explode and affect adjacent cells (def=no)? [TIB01] Name=Tiberium (Green) Tiberium=yes LegalTarget=false RadarInvisible=false [TIB02] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB03] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB04] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB05] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB06] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB07] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB08] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB09] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB10] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB11] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB12] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB13] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB14] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB15] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB16] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB17] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB18] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB19] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB20] Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false [TIB2_01] Image=TIB01 Name=Tiberium (Blue) Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_02] Image=TIB02 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_03] Image=TIB03 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_04] Image=TIB04 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_05] Image=TIB05 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_06] Image=TIB06 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_07] Image=TIB07 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_08] Image=TIB08 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_09] Image=TIB09 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_10] Image=TIB10 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_11] Image=TIB11 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_12] Image=TIB12 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_13] Image=TIB13 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_14] Image=TIB14 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_15] Image=TIB15 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_16] Image=TIB16 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_17] Image=TIB17 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_18] Image=TIB18 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_19] Image=TIB19 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB2_20] Image=TIB20 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_01] Image=TIB01 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_02] Image=TIB02 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_03] Image=TIB03 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_04] Image=TIB04 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_05] Image=TIB05 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_06] Image=TIB06 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_07] Image=TIB07 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_08] Image=TIB08 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_09] Image=TIB09 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_10] Image=TIB10 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_11] Image=TIB11 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_12] Image=TIB12 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_13] Image=TIB13 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_14] Image=TIB14 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_15] Image=TIB15 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_16] Image=TIB16 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_17] Image=TIB17 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_18] Image=TIB18 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_19] Image=TIB19 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [TIB3_20] Image=TIB20 Name=Tiberium Tiberium=yes LegalTarget=false RadarInvisible=false ChainReaction=yes [VEINHOLE] Name=Veinhole Monster LegalTarget=yes RadarColor=92,92,0 Land=Rock IsVeinholeMonster=true IsVeins=true NoUseTileLandType=true [VEINHOLEDUMMY] Name=Veinhole Monster Dummy Image=blahblahblah LegalTarget=no RadarColor=92,92,0 IsVeins=true [SROCK01] LegalTarget=false Name=Sand Rock #1 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [SROCK02] LegalTarget=false Name=Sand Rock #2 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [SROCK03] LegalTarget=false Name=Sand Rock #3 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [SROCK04] LegalTarget=false Name=Sand Rock #4 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [SROCK05] LegalTarget=false Name=Sand Rock #5 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [TROCK01] LegalTarget=false Name=Clear Rock #1 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false; IsARock=true ;WST 6/21/99 all pesky rocks need to have this [TROCK02] LegalTarget=false Name=Clear Rock #2 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [TROCK03] LegalTarget=false Name=Clear Rock #3 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [TROCK04] LegalTarget=false Name=Clear Rock #4 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [TROCK05] LegalTarget=false Name=Clear Rock #5 Land=Rock RadarColor=64,64,64 NoUseTileLandType=true DrawFlat=false IsARock=true ;WST 6/21/99 all pesky rocks need to have this [BTIB01] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB02] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB03] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB04] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB05] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB06] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB07] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB08] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB09] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB10] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB11] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [BTIB12] Name=Large Tiberium Tiberium=yes LegalTarget=false RadarColor=80,0,0 Land=Rock ChainReaction=yes CellAnim=BIGBLUE Image=None NoUseTileLandType=true DrawFlat=false [TRACKS01] Name=Track NwSe Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS02] Name=Track NeSw Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS03] Name=Track NS Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS04] Name=Track EW Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS05] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS06] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS07] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS08] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS09] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS10] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS11] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS12] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS13] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS14] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS15] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKS16] Name=Train Tracks Land=Railroad LegalTarget=false RadarColor=92,92,92 RadarInvisible = false [TRACKTUNNEL01] Name=Train Tracks Image=TRTUNN01 Land=Railroad LegalTarget=false RadarColor=92,92,92 [TRACKTUNNEL02] Name=Train Tracks Image=TRTUNN02 Land=Railroad LegalTarget=false RadarColor=92,92,92 [TRACKTUNNEL03] Name=Train Tracks Image=TRTUNN03 Land=Railroad LegalTarget=false RadarColor=92,92,92 [TRACKTUNNEL04] Name=Train Tracks Image=TRTUNN04 Land=Railroad LegalTarget=false RadarColor=92,92,92 [LOBRDG01] Image=LOBRDG01 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG02] Image=LOBRDG02 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG03] Image=LOBRDG03 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG04] Image=LOBRDG04 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG05] Image=LOBRDG05 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG06] Image=LOBRDG06 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG07] Image=LOBRDG07 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG08] Image=LOBRDG08 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG09] Image=LOBRDG09 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG10] Image=LOBRDG10 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG11] Image=LOBRDG11 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG12] Image=LOBRDG12 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG13] Image=LOBRDG13 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG14] Image=LOBRDG14 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG15] Image=LOBRDG15 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG16] Image=LOBRDG16 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG17] Image=LOBRDG17 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG18] Image=LOBRDG18 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG19] Image=LOBRDG19 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG20] Image=LOBRDG20 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG21] Image=LOBRDG21 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG22] Image=LOBRDG22 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG23] Image=LOBRDG23 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG24] Image=LOBRDG24 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG25] Image=LOBRDG25 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG26] Image=LOBRDG26 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=true RadarInvisible = false [LOBRDG27] Image=LOBRDG27 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=false RadarInvisible = true [LOBRDG28] Image=LOBRDG28 Name=Low Bridge Land=Road RadarColor=92,92,92 NoUseTileLandType=false RadarInvisible = true [LOBRDGE1] Image=LOBRDGE1 Name=Low Bridge End 1 Land=Road NoUseTileLandType=true RadarColor=92,92,92 [LOBRDGE2] Image=LOBRDGE2 Name=Low Bridge End 2 Land=Road NoUseTileLandType=true RadarColor=92,92,92 [LOBRDGE3] Image=LOBRDGE3 Name=Low Bridge End 3 Land=Road NoUseTileLandType=true RadarColor=92,92,92 [LOBRDGE4] Image=LOBRDGE4 Name=Low Bridge End 4 Land=Road NoUseTileLandType=true RadarColor=92,92,92 [VEINS] Image=VEINS Name=Tiberium Veins RadarColor=0,0,92 IsVeins=true Land=Weeds [RAILBRDG1] Image=RAILBRDG Name=Railroad Bridge 1 LegalTarget=true RadarColor=92,92,92 Overrides=yes NoUseTileLandType=false [RAILBRDG2] Image=RAILBRDG Name=Railroad Bridge 2 LegalTarget=true RadarColor=92,92,92 Overrides=yes NoUseTileLandType=false [BRIDGE1] Image=BRIDGE Name = Bridge 1 LegalTarget=true RadarColor=92,92,92 Overrides=yes NoUseTileLandType=false [BRIDGE2] Image=BRIDGE Name = Bridge 2 LegalTarget=true RadarColor=92,92,92 Overrides=yes NoUseTileLandType=false [CRATE] Name=Goodie Crate RadarColor=92,92,92 Crate=yes CrateTrigger=yes RadarInvisible=yes Land=Clear DrawFlat=false [CRAT01] Name=Crate LegalTarget=true RadarColor=92,92,92 ;Crate=yes ;CrateTrigger=yes ;RadarInvisible=yes Land=Rock [CRAT02] Name=Crate LegalTarget=true RadarColor=92,92,92 ;Crate=yes ;RadarInvisible=yes Land=Rock [CRAT03] Name=Crate LegalTarget=true RadarColor=92,92,92 ;Crate=yes ;RadarInvisible=yes Land=Rock [CRAT04] Name=Crate LegalTarget=true RadarColor=92,92,92 Land=Rock [CRAT0A] Name=Crate LegalTarget=true RadarColor=92,92,92 Land=Rock [CRAT0B] Name=Crate LegalTarget=true RadarColor=92,92,92 Land=Rock [CRAT0C] Name=Crate LegalTarget=true RadarColor=92,92,92 Land=Rock [DRUM01] Name=Drum LegalTarget=true RadarColor=92,92,92 Land=Rock [DRUM02] Name=Drum LegalTarget=true RadarColor=92,92,92 Land=Rock [PALET01] Name=Palette LegalTarget=true RadarColor=92,92,92 Land=Rock [PALET02] Name=Palette LegalTarget=true RadarColor=92,92,92 Land=Rock [PALET03] Name=Palette LegalTarget=true RadarColor=92,92,92 Land=Rock [PALET04] Name=Palette LegalTarget=true RadarColor=92,92,92 Land=Rock ; *** Smudge Objects *** ; This is the list of smudge objects. Typically, these include ; craters and scorch marks. [SmudgeTypes] 1=CR1 2=CR2 3=CR3 4=CR4 5=CR5 6=CR6 7=BURN01 8=BURN02 9=BURN03 10=BURN04 11=BURN05 12=BURN06 13=BURN07 14=BURN08 15=BURN09 16=BURN10 17=BURN11 18=BURN12 19=BURN13 20=BURN14 21=BURN15 22=BURN16 23=BURNT01 24=BURNT02 25=BURNT03 26=BURNT04 27=BURNT05 28=BURNT06 29=BURNT07 30=BURNT08 31=BURNT09 32=BURNT10 33=BURNT11 34=BURNT12 35=CRATER01 36=CRATER02 37=CRATER03 38=CRATER04 39=CRATER05 40=CRATER06 41=CRATER07 42=CRATER08 43=CRATER09 44=CRATER10 45=CRATER11 46=CRATER12 ; These specify the objects (actually more of an artwork ; element than a game object) called smudges. These typically ; are used to mark the effects of battle. ; Crater = Is this a crater smudge [special growth logic] (def=no)? [CRATER01] Crater=yes [CRATER02] Crater=yes [CRATER03] Crater=yes [CRATER04] Crater=yes [CRATER05] Crater=yes [CRATER06] Crater=yes [CRATER07] Crater=yes [CRATER08] Crater=yes [CRATER09] Crater=yes [CRATER10] Crater=yes [CRATER11] Crater=yes Width=2 Height=2 [CRATER12] Crater=yes Width=2 Height=2 [BURNT01] Burn=yes [BURNT02] Burn=yes [BURNT03] Burn=yes [BURNT04] Burn=yes [BURNT05] Burn=yes [BURNT06] Burn=yes [BURNT07] Burn=yes Width=2 [BURNT08] Burn=yes Width=2 [BURNT09] Burn=yes Height=2 [BURNT10] Burn=yes Height=2 [BURNT11] Burn=yes Width=2 Height=2 [BURNT12] Burn=yes Width=2 Height=2 [CR1] [CR2] [CR3] [CR4] [CR5] [CR6] [BURN01] [BURN02] [BURN03] [BURN04] [BURN05] [BURN06] [BURN07] [BURN08] [BURN09] [BURN10] [BURN11] [BURN12] [BURN13] [BURN14] [BURN15] [BURN16] ; ******* Land Characteristics ******* ; This section specifies the characteristics of the various ; terrain types. The primary purpose is to differentiate the ; movement capabilities. ; Float = % of full speed for ships [0 means impassable] (def=100) ; Foot = % of full speed for foot soldiers [0 means impassable] (def=100) ; Track = % of full speed for tracked vehicles [0 means impassable] (def=100) ; Wheel = % of full speed for wheeled vehicles [0 means impassable] (def=100) ; Hover = % of full speed for hovering vehicles [0 means impassable] (def=100) ; Amphibious = % of full speed for amphibious vehicles [0 impassable] (def=100) ; Buildable = Can buildings be built upon this terrain (def=no)? ; clear grassy terrain [Clear] Foot=90% Track=70% Wheel=70% Float=0% Hover=100% Amphibious=80% Creep=100% Buildable=yes ; rocky terrain [Rough] Foot=80% Track=60% Wheel=40% Float=0% Hover=100% Amphibious=40% Creep=90% Buildable=yes ; roads [Road] Foot=100% Track=100% Wheel=100% Hover=100% Float=0% Amphibious=100% Creep=100% Buildable=yes ; open water [Water] Foot=0% Track=0% Wheel=0% Hover=100% Float=100% Amphibious=80% Creep=0% Buildable=no ; cliffs [Rock] Foot=0% Track=0% Wheel=0% Float=0% Hover=0% Amphibious=0% Creep=0% Buildable=no ; walls and other man made obstacles [Wall] Foot=0% Track=0% Wheel=0% Float=0% Hover=0% Amphibious=0% Creep=0% Buildable=no ; Tiberium [Tiberium] Foot=90% Track=70% Wheel=50% Float=0% Hover=100% Amphibious=50% Creep=100% Buildable=no ; Vein hole creater weeds [Weeds] Foot=50% Track=70% Wheel=50% Float=0% Hover=100% Amphibious=50% Creep=90% Buildable=no ; sandy beach [Beach] Foot=0% Track=0% Wheel=0% Float=0% Hover=100% Amphibious=60% Creep=0% Buildable=no ; ice [Ice] Foot=50% Track=80% Wheel=50% Float=0% Hover=100% Amphibious=50% Creep=100% Buildable=no ; train tracks [Railroad] Foot=90% Track=100% Wheel=50% Float=0% Hover=100% Amphibious=50% Creep=100% Buildable=no ; tunnels [Tunnel] Foot=100% Track=100% Wheel=100% Float=0% Hover=100% Amphibious=100% Creep=100% Buildable=no ; ******* Random Crate Powerups ******* ; This specifies the chance for the specified crate powerup to appear ; in a 'random' crate. The chance is expressed in the form of 'shares' ; out of the total shares specified. The second parameter is the animation ; to use when this crate is picked up. The third parameter, if present, specifies ; the data value needed for that crate powerup. They mean different things ; for the different powerups. [Powerups] Armor=33,ARMOR,0.5 ; armor of nearby objects increased (armor multiplier) Cloak=20,CLOAK ; enable cloaking on nearby objects Darkness=5,SHROUDX ; cloak entire radar map Explosion=38,,500 ; high explosive baddie (damage per explosion) Firepower=28,FIREPOWR,2.0 ; firepower of nearby objects increased (firepower multiplier) HealBase=23,HEALALL ; all buildings to full strength ICBM=13,MLTIMISL ; nuke missile one time shot (was CHEMISLE) Money=55,MONEY,2000 ; a chunk o' cash (maximum cash) Napalm=25,,600 ; fire explosion baddie (damage) Reveal=8,REVEAL ; reveal entire radar map Speed=30,ARMOR,1.7 ; speed of nearby objects increased (speed multiplier) Squad=45, ; squad of random infantry Unit=40, ; vehicle Invulnerability=10,ARMOR,1.0 ; invulnerability (duration in minutes) Veteran=15,VETERAN,1 ; veteran upgrade (levels to upgrade) IonStorm=0, ; initiate ion storm Gas=18,,100 ; tiberium gas (damage for each gas cloud) Tiberium=35, ; tiberium patch Pod=0, ; drop pod special ; ******* Tiberium Varieties ******* ; There are various kinds of tiberium. This lists their number and ; particulars. [Tiberiums] 0=Riparius 1=Cruentus 2=Vinifera 3=Aboreus ; Name = display name ; Image = image to use [1=small, 2=large, 3=vine] ; Value = credit value per 'bail' ; Growth = growth rate ; Spread = spread rate ; Power = explosive power per 'bail' (def=0) ; Color = display color of the Tiberium ; Shard = crystal to fly off when chain reacting (def=none) ; This is green tiberium. It grows and spreads quickly and is not explosive [Riparius] Name=Tiberium Riparius Image=1 Power=1 ;4 Value=25 Growth=2200 GrowthPercentage=.09 Spread=2200 SpreadPercentage=.09 Color=NeonGreen ; **WARNING**: If you change this color, notify Bret_a ; This is the big tiberium crystal. It does not grow or spread, is impassable and is explosive ; Not currently in use in TS (AI) [Cruentus] Name=Tiberium Cruentus Image=2 Value=70 Growth=10000 GrowthPercentage=0 Spread=10000 SpreadPercentage=0 Power=1 ;10 Color=NeonBlue ; **WARNING**: If you change this color, notify Bret_a Debris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4 ; This is blue tiberium. It grows and spreads slowly and is explosive. [Vinifera] Name=Tiberium Vinifera Image=3 Value=40 Growth=10000 GrowthPercentage=.05 Spread=10000 SpreadPercentage=.05 Power=20 ;100 Color=NeonBlue ; **WARNING**: If you change this color, notify Bret_a Debris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4 ; This is blue tiberium. It grows and spreads slowly and is explosive. This entry should be ; the same as [vinifera] except for Name and Image [Aboreus] Name=Tiberium Aboreus Image=4 Value=30 Growth=10000 GrowthPercentage=.05 Spread=10000 SpreadPercentage=.05 Power=1 ;10 Color=NeonBlue ; **WARNING**: If you change this color, notify Bret_a Debris=CRYSTAL1,CRYSTAL2,CRYSTAL3,CRYSTAL4 ; ******* Mission Control ******* ; This specifies the various general behavior characteristics of ; the missions that objects can be assigned. Each of the game objects must ; be in a mission. The mission behavior is generally hard coded into ; the program, but there are some behavior characteristics that can ; be overridden. Don't modify these. ; NoThreat = Is its weapons disabled and thus ignored as a potential target until fired upon (def=no)? ; Zombie = Is forced to sit there like a zombie and never recovers (def=no)? ; Recruitable = Can it be recruited into a team or base defense (def=yes)? ; Paralyzed = Is the object frozen in place but can still fire and function (def=no)? ; Retaliate = Is allowed to retaliate while on this mission (def=yes)? ; Scatter = Is allowed to scatter from threats (def=yes)? ; Rate = delay between normal processing (larger = faster game, less responsiveness) ; AARate = anti-aircraft delay rate (if not specifed it uses regular rate). ; Unit sits still and plays dead. [Sleep] Recruitable=no Zombie=yes Retaliate=no Scatter=no Rate=1 ; Unit doesn't fire and is not considered a threat. [Harmless] Recruitable=no NoThreat=yes Retaliate=no Rate=.5 ; Just like guard mode, but cannot move. [Sticky] Recruitable=no Paralyzed=yes Scatter=no Rate=.016 ; Special attack mission used by team logic. [Attack] Rate=.016 AARate=.016 ; Move to destination. [Move] Rate=.016 ; Patrol a series of waypoints [Patrol] Rate=.016 ; Special move to destination after all other queued moves occur. [QMove] Rate=.016 ; Run away (possibly leave the map). [Retreat] Recruitable=no Retaliate=no Rate=.1 ; Sit around and engage any enemy that wanders within weapon range. [Guard] Rate=.030 AARate=.016 ; Enter building or transport for loading purposes. [Enter] Retaliate=no Recruitable=no Rate=.016 ; Engineer entry logic. [Capture] Retaliate=no Recruitable=no Scatter=no Rate=.016 ; Handle harvest ore - dump at refinery loop. [Harvest] Retaliate=no Recruitable=no Scatter=no Rate=.016 ; Guard the general area where the unit starts at. [Area Guard] Recruitable=yes Rate=.040 AARate=.032 ; [Return] ; Stop moving and firing at the first available opportunity. [Stop] ; [Ambush] ; Scan for and attack any enemies whereever they may be. [Hunt] Recruitable=no Retaliate=no Rate=.016 ; While dropping off cargo (e.g., APC unloading passengers). [Unload] Recruitable=no Retaliate=no Scatter=no Rate=.016 ; Tanya running to place bomb in building. [Sabotage] Recruitable=no Rate=.016 ; Buildings use this when building up after initial placement. [Construction] Recruitable=no Retaliate=no Scatter=no ; Buildings use this when deconstruction after being sold. [Selling] Recruitable=no NoThreat=yes Retaliate=no Scatter=no ; Service depot uses this mission to repair attached object. [Repair] Rate=.08 ; Special team override mission. [Rescue] Rate=.016 ; Missile silo special launch missile mission. [Missile] Rate=.1 ; While opening or closing a gate to allow passage. [Open] Rate=.016 ; *** Voxel Animation List *** ; This is the complete list of voxel animations available. ; VoxelAnims are meant to be flying debris. Things like ; turrets and tires make good voxel anims. [VoxelAnims] 1=PIECE 2=TIRE 3=GASTANK 4=SONICTURRET 5=4TNKTURRET 6=CRYSTAL01 7=CRYSTAL02 8=METEOR01 9=METEOR02 10=PEBBLE ; ******* Voxel Debris types ******* ; Translucent = is the debris to be drawn with translucency (def=no)? ; Elasticity = "bounciness" of the object [should be 0.0 through 1.0] (def=0.75) ; MinAngularVelocity = minimum rate at which the debris is to spin in degrees (def=0.0) ; MaxAngularVelocity = maximum rate at which the debris is to spin in degrees (def=10.0) ; Duration = max number of frames to let the debris exist (def=30) ; MinZVel = minimum starting Z velocity (def=3.5) ; MaxZVel = maximum starting Z velocity (def=5.0) ; MaxXYVel = maximim starting lateral velocity (def=15.0) ; ShareBodyData = Get the voxel data from another Type's body voxel data (def = no)? ; ShareTurretData = Get the voxel data from another Type's turret voxel data (def = no)? ; ShareBarrelData = Get the voxel data from another Type's barrel voxel data (def = no)? ; ShareSource = name of the object to share voxel data from (def = none) ; VoxelIndex = voxel piece within the voxel data to use (def = 0) ; StartSound = sound to play when the voxel anim is created (def = VOC_NONE). ; BounceSound = sound to play when the voxel anim bounces (def = VOC_NONE). ; ExpireSound = sound to play when the voxel anim expires (def = VOC_NONE). ; BounceAnim = animation to launch when the voxel anim bounces (def = ANIM_NONE). ; ExpireAnim = animation to launch when the voxel anim expires (def = ANIM_NONE). ; TrailerAnim = animation to trail behind the object (usually smoke or flame) ; DamageRadius = the debris damages objects that it hits if they're closer than this distance ; Damage = amount of damage to apply to objects that are hit ; Warhead = warhead to use for damage purposes ; AttachedSystem = particle system to attach to the voxel anim ; Spawns = the particle spawned when this voxel debris explodes (def = none) ; SpawnCount = number of particles spawned [on average] (def = 0) [PIECE] Name=Scrap Metal Debris Elasticity=0 MinAngularVelocity=5.0 MaxAngularVelocity=9.0 MinZVel=24.0 MaxZVel=28.0 MaxXYVel=15.0 Duration=75 Damage=5 ExpireAnim=TWLT036 DamageRadius=100 Warhead=TankOGas [TIRE] Name=Flying Tire Elasticity=0.8 MinAngularVelocity=12.0 MaxAngularVelocity=24.0 MinZVel=28.0 MaxZVel=32.0 MaxXYVel=10.0 Duration=150 [GASTANK] Name=Flying Gas Tank Elasticity=0.0 MinAngularVelocity=9.0 MaxAngularVelocity=15.0 MinZVel=30.0 MaxZVel=35.0 MaxXYVel=8.0 Duration=100 ExpireAnim=TWLT036 Damage=20 DamageRadius=100 Warhead=TankOGas [SONICTURRET] Name=Disruptor Turret ShareTurretData=yes ShareSource=SONIC Elasticity=0.0 MinAngularVelocity=10.0 MaxAngularVelocity=14.0 MinZVel=30.0 MaxZVel=38.0 MaxXYVel=8.0 Duration=100 ExpireAnim=TWLT026 Damage=90 DamageRadius=100 Warhead=TankOGas [4TNKTURRET] Name=Mammoth Tank Turret ShareTurretData=yes ShareSource=4TNK Elasticity=0.0 MinAngularVelocity=10.0 MaxAngularVelocity=14.0 MinZVel=30.0 MaxZVel=38.0 MaxXYVel=8.0 Duration=100 ExpireAnim=TWLT036 Damage=30 DamageRadius=50 Warhead=TankOGas [CRYSTAL01] Name=TiberiumCrystal01 ShareTurretData=yes ShareSource=SONIC Elasticity=0.0 MinAngularVelocity=12.0 MaxAngularVelocity=24.0 MinZVel=28.0 MaxZVel=32.0 MaxXYVel=10.0 Duration=150 ExpireAnim=TWLT050 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true [CRYSTAL02] Name=TiberiumCrystal02 Image=GASTANK Elasticity=0.0 MinAngularVelocity=12.0 MaxAngularVelocity=24.0 MinZVel=40.0 MaxZVel=45.0 MaxXYVel=18.0 Duration=150 ExpireAnim=TWLT050 Damage=40 DamageRadius=100 Warhead=TankOGas IsTiberium=true [METEOR01] Name=Meteorite01 Image=MTRS Elasticity=0.0 MinAngularVelocity=12.0 MaxAngularVelocity=30.0 MinZVel=-100.0 MaxZVel=-100.0 MaxXYVel=100.0 Duration=70 ExpireAnim=TWLT070 Damage=500 DamageRadius=300 Warhead=Meteorite IsMeteor=true Spawns=PEBBLE SpawnCount=5 [METEOR02] Name=Meteorite02 Image=MTRB Elasticity=0.0 MinAngularVelocity=12.0 MaxAngularVelocity=30.0 MinZVel=-100.0 MaxZVel=-100.0 MaxXYVel=100.0 Duration=70 ExpireAnim=TWLT100 Damage=500 DamageRadius=300 Warhead=Meteorite IsMeteor=true IsTiberium=true Spawns=PEBBLE SpawnCount=7 [PEBBLE] Name=TiberiumShard Image=MTRT Elasticity=0.0 MinAngularVelocity=12.0 MaxAngularVelocity=24.0 MinZVel=40.0 MaxZVel=45.0 MaxXYVel=18.0 Duration=150 ExpireAnim=TWLT036 Damage=20 DamageRadius=100 Warhead=TankOGas IsTiberium=true ; ******* Special Weapon types ******* ; This is a list of the various types of super weapons available in the game [SuperWeaponTypes] 1=MultiSpecial 2=EMPulseSpecial 3=FirestormSpecial 4=IonCannonSpecial 5=HuntSeekSpecial 6=ChemicalSpecial 7=DropPodSpecial ; IsPowered -- does this super weapon become inoperative in a low power situation? ; RechargeVoice -- Voice to use when weapon is fully recharged and ready. ; ChargingVoice -- Voice to use when weapon begins charging. ; ImpatientVoice -- Voice to use when user clicks on weapon that isn't finished charging. ; SuspendVoice -- Voice to use when special weapon charging is suspended. ; RechargeTime -- time in minutes to recharge this special ; Chem weapon. The logic will fail if this weapon is 'powered' [ChemicalSpecial] Name=Chemical Missile IsPowered=false RechargeVoice=00-I152 ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=.3 Type=ChemMissile SidebarImage=ChemIcon Action=ChemBomb ManualControl=yes WeaponType=ChemLauncher AuxBuilding=NAWAST [MultiSpecial] Name=Multi-Missile IsPowered=true RechargeVoice=00-I154 ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=10 Type=MultiMissile SidebarImage=MltiIcon Action=Nuke WeaponType=MultiLauncher [EMPulseSpecial] Name=E.M. Pulse IsPowered=true RechargeVoice=00-I158 ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=4.5 Type=EMPulse SidebarImage=PulsIcon Action=EMPulse [FirestormSpecial] Name=Firestorm Defense IsPowered=true RechargeVoice=00-I162 ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=3 Type=Firestorm SidebarImage=FSTDICON UseChargeDrain=true [IonCannonSpecial] Name=Ion Cannon IsPowered=true RechargeVoice=00-I156 ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=8.5 Type=IonCannon Action=IonCannon SidebarImage=IONCICON [HuntSeekSpecial] Name=Hunter Seeker IsPowered=true RechargeVoice= ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=12 Type=HunterSeeker SidebarImage=DETNICON [DropPodSpecial] Name=Drop Pod IsPowered=true RechargeVoice=00-I506 ChargingVoice= ImpatientVoice= SuspendVoice= RechargeTime=7 ;was 6 Type=DropPod Action=DropPod SidebarImage=PODSICON ; ******* Globals Variable Names ******* ; These must be constant throughout all scenarios based on these ; rules. These are numbered starting from zero. Do not change the ; number values or else all preexisting triggers using them will ; break. [VariableNames] 0= 1= 2= 3=Sensor Array Down 4=Toxin Trucks Found 5=Dam Destroyed 6=Ion Cannon Codes Found 7=C4 Placed 8=Completed 3B 9=Prisoners Freed 10=Train Stolen 11=Completed 9B 12=Difficulty Easy 13=Difficulty Medium 14=Difficulty Hard 15=Difficulty Easy/Medium 16=Difficulty Medium/Hard ================================================ FILE: DXMainClient/Resources/INI/snow.ini ================================================ ;Modified July 30, 2002 ;Marble madness set added ;By DJBREIT ; Version 1.0 ; ; ***Tiberian Sun Isometric Tile Control File*** ; ; ; General section. ; ; RampBase ; Number of tile set that includes all the ramp types ; ; MMRampBase ; Number of tile set that has the marble madness mode ramps ; ; ClearTile ; Number of tile set to use for clear terrain ; ; RoughTile ; Number of tile set that has the rough terrain ; ; ClearToRoughLAT ; Tile set that has the 16 tiles for the clear/rough LAT system ; ; HeightBase ; First tile of marble madness height tiles ; ; BlackTile ; Black tile used when rendering non-existent cells ; ; BridgeSet ; Tile set that contains bridge edges ; ; BridgeTopLeft1 ; BridgeTopLeft2 ; BridgeBottomRight1 ; BridgeBottomRight2 ; BridgeTopRight1 ; BridgeTopRight2 ; BridgeBottomLeft1 ; BridgeBottomLeft2 ; Tiles in bridge set to search for when fixing up bridges ; ; [General] PaveTile = 68 MiscPaveTile = 69 ClearToPaveLat = 70 RampBase = 9 MMRampBase = 7 RampSmooth = 41 ClearTile = 0 RoughTile = 13 ClearToRoughLat = 14 HeightBase = 46 CliffSet = 10 ShorePieces = 12 WaterSet = 21 Ice1Set = 31 Ice2Set = 32 Ice3Set = 33 IceShoreSet = 34 BlackTile = 6 BridgeSet = 19 TrainBridgeSet = 39 SlopeSetPieces = 25 SlopeSetPieces2 = 26 MonorailSlopes = 45 Tunnels=47 TrackTunnels = 49 DirtTunnels = 66 DirtTrackTunnels = 67 WaterfallEast = 35 WaterfallWest = 37 WaterfallNorth = 36 WaterfallSouth = 30 CliffRamps = 25 PavedRoads = 20 PavedRoadEnds = 38 DirtRoadJunction = 17 DirtRoadCurve = 16 DirtRoadStraight = 18 RoughGround = 40 WaterCliffs = 15 DirtRoadSlopes = 23 DestroyableCliffs = 61 SandTile = 62 ClearToSandLat = 63 GreenTile = 64 ClearToGreenLat = 65 Rocks=62 BridgeTopLeft1 = 1 BridgeTopLeft2 = 2 BridgeBottomRight1 = 3 BridgeBottomRight2 = 3 BridgeTopRight1 = 4 BridgeTopRight2 = 5 BridgeBottomLeft1 = 6 BridgeBottomLeft2 = 6 BridgeMiddle1 = 7 BridgeMiddle2 = 12 ; ; TS Will scan through this file when loading up a theater and read in the ; isometric tile files specified. ; ; [TileSetnnnn] ; This is the tile set section header. TS will loop through from TileSet0000 ; upwards until it finds a set that hasnt been specified. ; ; SetName ; The name of the set as it will appear in the editor. ; ; FileName ; The base file name of each file in the set. The files in a set must all ; have the same basic name with a 2 digit id number appended. For example ; cliff01.tem, cliff02.tem, cliff03.tem. The 2 digit number starts at 01 ; not 00. ; ; TilesInSet ; The number of files comprising the set. There is a practical limit of ; 99 due to the 2 digit file name suffix. ; ; LastTilesInSet ; The number of tiles which the set used to have. This tells the ; game that the number of tiles in the set has changed and it should fix up ; the tile numbers when a map is loaded. If the map is then saved again, ; it will be saved with the correct tile numbers. This value should only ; be used to load up maps, convert the tile numbers, then save the maps ; out again. Then the LastTilesInSet entry should be removed or the newly ; fixed up maps will not load correctly. ; ; MarbleMadness ; The section number of the tile set to use for these tiles when in ; marble madness mode. ; ; NonMarbleMadness ; For marble madness tiles, this is the tile set to use when not in ; marble madness mode. ; ; Morphable ; Can this tile set be modified using the raise/lower ground function? ; ; ShadowCaster ; Do the tiles in this set cast shadows (cliff pieces) ; ; ToTemperateTheater ; The equivilent tile section in the temperate theater ; ; ToSnowTheater ; The equivilent tile section in the snow theater ; ; LowRadarColor ; What color to show on the radar for this set at the lowest height ; ; HighRadarColor ; What color to show on the radar for this set at the highest height ; ; Blank tile for filling in holes. ; [TileSet0000] SetName = Clear FileName = Clear TilesInSet = 1 Morphable = true LowRadarColor=150,150,192 HighRadarColor=200,200,255 RequiredForRMG = true AllowTiberium = true ; ; A few buildings ; [TileSet0001] SetName = Misc Buildings FileName = Bld TilesInSet = 3 LowRadarColor=150,150,192 HighRadarColor=200,200,255 AllowBurrowing=false ; ; Some basic flat tiles ; [TileSet0002] SetName = Clear FileName = Snow TilesInSet = 4 Morphable = true LowRadarColor=150,150,192 HighRadarColor=200,200,255 RequiredForRMG = true AllowTiberium = true ; ; A couple of old cliff pieces (not used) ; [TileSet0003] SetName = Cliff Pieces FileName = clif TilesInSet = 2 LowRadarColor=150,150,192 HighRadarColor=200,200,255 AllowBurrowing=false ; ; A large ice flow. ; [TileSet0004] SetName = Ice Flow FileName = flow TilesInSet = 1 LowRadarColor=192,192,192 HighRadarColor=255,255,255 AllowBurrowing=false ; ; A nice little house. ; [TileSet0005] SetName = House FileName = house TilesInSet = 1 AllowBurrowing=false ; ; Blank tile used for filling areas with no cell data. ; [TileSet0006] SetName = Blank FileName = blank TilesInSet = 1 Morphable = true LowRadarColor=150,150,192 HighRadarColor=200,200,255 RequiredForRMG = true AllowTiberium = true ; ; Marble madness mode ramp pieces. ; [TileSet0007] SetName = MM Ramps FileName = mslop TilesInSet = 20 NonMarbleMadness = 9 Morphable = true ;LastTilesInSet = 16 LowRadarColor=150,150,192 HighRadarColor=200,200,255 ; ; Height pieces for marble madness mode. ; ; Obsolete. Replaced with HITE01 - HITE10 ; [TileSet0008] SetName = Height Pieces FileName = mslop TilesInSet = 7 Morphable = true AllowTiberium = true ; ; Misc theater ramps ; [TileSet0009] SetName = Ice Ramps FileName = slope TilesInSet = 20 MarbleMadness = 7 Morphable = true ;LastTilesInSet = 16 LowRadarColor=150,150,192 HighRadarColor=200,200,255 RequiredForRMG = true AllowTiberium = true ; ; Cliff set. ; [TileSet0010] SetName = Cliff Set FileName = Cliff TilesInSet = 40 MarbleMadness = 22 ShadowCaster = true ShadowTiles = 40 LowRadarColor=110,110,150 HighRadarColor=150,150,190 AllowBurrowing=false RequiredForRMG = true ; ; Civilian buildings ; [TileSet0011] SetName = Civilian Buildings FileName = Civ TilesInSet = 8 AllowBurrowing=false ; ; Shore pieces ; [TileSet0012] SetName = Shore Pieces FileName = Shore TilesInSet = 42 LowRadarColor=80,80,150 HighRadarColor=80,80,150 MarbleMadness=53 AllowBurrowing=false RequiredForRMG = true ; ; Clear terrain (slightly rough) ; [TileSet0013] SetName = Rough lat FileName = Ruff TilesInSet = 1 Morphable = true LowRadarColor=130,130,192 HighRadarColor=130,130,192 RequiredForRMG = true AllowTiberium = true ; ; L.A.T. system for connecting clear and rough clear terrain ; [TileSet0014] SetName = Clear/Rough LAT FileName = clat TilesInSet = 16 Morphable = true LowRadarColor=140,140,192 HighRadarColor=140,140,192 RequiredForRMG = true AllowTiberium = true ; ; Cliff pieces that meet water pieces ; [TileSet0015] SetName = Cliff/Water pieces FileName = WCliff TilesInSet = 28 ShadowCaster = true ShadowTiles = 28 LowRadarColor=70,70,120 HighRadarColor=90,90,200 MarbleMadness=58 AllowBurrowing=false ; ; Dirt roads. Corner pieces. ; [TileSet0016] SetName = Bendy Dirt Roads FileName = Droadc TilesInSet = 24 LowRadarColor=110,80,0 HighRadarColor=130,90,0 MarbleMadness=50 RequiredForRMG = true AllowTiberium = true ; ; Dirt roads. Junctions. ; [TileSet0017] SetName = Dirt Road Junctions FileName = Droadj TilesInSet = 11 LowRadarColor=110,80,0 HighRadarColor=130,90,0 MarbleMadness=51 RequiredForRMG = true AllowTiberium = true ; ; Dirt roads. Straights. ; [TileSet0018] SetName = Straight Dirt Roads FileName = Droads TilesInSet = 66 LowRadarColor=110,80,0 HighRadarColor=130,90,0 MarbleMadness=52 RequiredForRMG = true AllowTiberium = true ; ; Bridge sections. ; [TileSet0019] SetName = Bridges FileName = Ovrps TilesInSet = 16 LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false ; ; Paved roads. ; [TileSet0020] SetName = Paved Roads FileName = Proad TilesInSet = 21 LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false RequiredForRMG = true ; ; Just icy water. ; [TileSet0021] SetName = Water FileName = Water TilesInSet = 14 LowRadarColor=10,10,80 HighRadarColor=15,15,110 MarbleMadness=60 AllowBurrowing=false RequiredForRMG = true ; ; Cliff set. ; [TileSet0022] SetName = Marble Madness Cliff Set FileName = Mclif TilesInSet = 40 NonMarbleMadness = 10 ShadowCaster = true ShadowTiles = 40 LowRadarColor=110,110,150 HighRadarColor=150,150,190 AllowBurrowing=false ; ; Dirt road slopes ; [TileSet0023] SetName = Dirt Road Slopes FileName = DRSLPE TilesInSet = 8 MarbleMadness = 24 LowRadarColor=110,80,0 HighRadarColor=130,90,0 RequiredForRMG = true AllowTiberium = true ; ; Marble Madness dirt road slopes ; [TileSet0024] SetName = MM Dirt Road Slopes FileName = MDRSLP TilesInSet = 8 NonMarbleMadness = 23 LowRadarColor=110,80,0 HighRadarColor=130,90,0 AllowTiberium = true ; ; Slope set pieces ; [TileSet0025] SetName = Slope Set Pieces FileName = RAMP TilesInSet = 10 MarbleMadness = 26 LowRadarColor=150,150,192 HighRadarColor=200,200,255 ShadowCaster = true ShadowTiles = 10 RequiredForRMG = true ; ; Slope set pieces - Marble Madness version ; [TileSet0026] SetName = Slope Set Pieces FileName = MRAM TilesInSet = 10 NonMarbleMadness = 25 LowRadarColor=150,150,192 HighRadarColor=200,200,255 ; ; A dead oil tanker ; [TileSet0027] SetName = Dead Oil Tanker FileName = TANKER TilesInSet = 1 AllowBurrowing=false ; ; Some ruins ; [TileSet0028] SetName = Ruins FileName = RUIN TilesInSet = 1 AllowBurrowing=false ; ; Height pieces for marble madness mode ; Replaced with 15 variation version. ; [TileSet0029] SetName = Obsolete Height Pieces FileName = hyte TilesInSet = 10 Morphable = true LowRadarColor=150,150,192 HighRadarColor=200,200,255 AllowToPlace=no AllowTiberium = true ; ; Waterfalls. ; [TileSet0030] SetName = Waterfalls FileName = W-a- TilesInSet = 4 MarbleMadness=54 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true [TileSet0031] SetName = Ice 01 FileName = Ice01 TilesInSet = 64 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true MarbleMadness=31 [TileSet0032] SetName = Ice 02 FileName = Ice02 TilesInSet = 64 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true MarbleMadness=32 [TileSet0033] SetName = Ice 03 FileName = Ice03 TilesInSet = 64 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true MarbleMadness=33 [TileSet0034] SetName = Ice shore FileName = Ishore TilesInSet = 48 LowRadarColor=200,200,230 HighRadarColor=200,200,230 AllowBurrowing=false RequiredForRMG = true MarbleMadness=34 [TileSet0035] SetName = Waterfalls-B FileName = W-b- TilesInSet = 4 ToTemperateTheater=49 MarbleMadness=55 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true [TileSet0036] SetName = Waterfalls-C FileName = W-c- TilesInSet = 4 ToTemperateTheater=50 MarbleMadness=56 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true [TileSet0037] SetName = Waterfalls-D FileName = W-d- TilesInSet = 4 ToTemperateTheater=51 MarbleMadness=57 LowRadarColor=240,240,255 HighRadarColor=240,240,255 AllowBurrowing=false RequiredForRMG = true [TileSet0038] SetName = Paved Road Ends FileName = p_end TilesInSet = 4 ToTemperateTheater = 36 Morphable = false LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false RequiredForRMG = true ; ; Train Bridge sections. ; [TileSet0039] SetName = TrainBridges FileName = Tovrps TilesInSet = 16 LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false [TileSet0040] SetName = Rough ground FileName = Rough TilesInSet = 10 Morphable = false ToTemperateTheater = 35 LowRadarColor=120,120,150 HighRadarColor=120,120,150 RequiredForRMG = true [TileSet0041] SetName = Ramp edge fixup FileName = Rmpfx TilesInSet = 12 Morphable = true ToTemperateTheater = 43 MarbleMadness = 42 LowRadarColor=150,150,192 HighRadarColor=200,200,255 RequiredForRMG = true AllowTiberium = true [TileSet0042] SetName = Ramp edge fixup - Marble Madness FileName = Mrmfx TilesInSet = 12 Morphable = true ToTemperateTheater = 44 NonMarbleMadness = 41 LowRadarColor=150,150,192 HighRadarColor=200,200,255 AllowTiberium = true [TileSet0043] SetName = Water slopes FileName = WSLOPE TilesInSet = 4 Morphable = no ToSnowTheater = 45 MarbleMadness=59 AllowBurrowing=false [TileSet0044] SetName = Paved Road Slopes FileName = Prslpe TilesInSet = 4 Morphable = no ToTemperateTheater = 47 LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false [TileSet0045] SetName = Monorail Slopes FileName = Tslope TilesInSet = 4 Morphable = no ToTemperateTheater = 48 LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false MarbleMadness=73 [TileSet0046] SetName = Newest MM Height Pieces FileName = hyte TilesInSet = 15 Morphable = true LowRadarColor=150,150,192 HighRadarColor=200,200,255 AllowTiberium = true [TileSet0047] SetName = Tunnel Floor FileName = tunnel TilesInSet = 4 Morphable = no ToTemperateTheater = 53 LowRadarColor = 100,100,100 HighRadarColor = 100,100,100 AllowBurrowing=false MarbleMadness=71 [TileSet0048] SetName = Tunnel Side FileName = tunnex TilesInSet = 2 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToTemperateTheater = 54 AllowBurrowing=false MarbleMadness=72 [TileSet0049] SetName = TrackTunnel Floor FileName = tunnet TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToTemperateTheater = 55 AllowBurrowing=false MarbleMadness=71 ; ; Dirt roads. Corner pieces. Marble Madness version. ; [TileSet0050] SetName = MM Bendy Dirt Roads FileName = MDrodc TilesInSet = 24 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness = 16 ToTemperateTheater = 58 AllowTiberium = true ; ; Dirt roads. Junctions. Marble Madness version. ; [TileSet0051] SetName = MM Dirt Road Junctions FileName = MDrodj TilesInSet = 11 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness = 17 ToTemperateTheater = 59 AllowTiberium = true ; ; Dirt roads. Straights. Marble Madness version. ; [TileSet0052] SetName = MM Straight Dirt Roads FileName = MDrods TilesInSet = 66 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness = 18 ToTemperateTheater = 60 AllowTiberium = true ; ; Shore pieces ; [TileSet0053] SetName = Shore Pieces FileName = MShore TilesInSet = 42 LowRadarColor=80,80,150 HighRadarColor=80,80,150 NonMarbleMadness=12 AllowBurrowing=false ; ; Waterfalls. MM. ; [TileSet0054] SetName = MM Waterfalls FileName = MWa- TilesInSet = 4 ToTemperateTheater = 62 LowRadarColor=240,240,255 HighRadarColor=240,240,255 NonMarbleMadness=30 AllowBurrowing=false [TileSet0055] SetName = MM Waterfalls-B FileName = MWb- TilesInSet = 4 ToTemperateTheater = 63 LowRadarColor=240,240,255 HighRadarColor=240,240,255 NonMarbleMadness=35 AllowBurrowing=false [TileSet0056] SetName = MM Waterfalls-C FileName = MWc- TilesInSet = 4 ToTemperateTheater = 64 LowRadarColor=240,240,255 HighRadarColor=240,240,255 NonMarbleMadness=36 AllowBurrowing=false [TileSet0057] SetName = MM Waterfalls-D FileName = MWd- TilesInSet = 4 ToTemperateTheater = 65 LowRadarColor=240,240,255 HighRadarColor=240,240,255 NonMarbleMadness=37 AllowBurrowing=false ; ; Cliff pieces that meet water pieces ; [TileSet0058] SetName = MM Cliff/Water pieces FileName = MWClif TilesInSet = 28 ;ShadowCaster = true ;ShadowTiles = 22 LowRadarColor=70,70,120 HighRadarColor=90,90,200 NonMarbleMadness=15 ToTemperateTheater=67 AllowBurrowing=false [TileSet0059] SetName = MM Water slopes FileName = MWSLOP TilesInSet = 4 Morphable = no ToTemperateTheater = 68 MarbleMadness=59 NonMarbleMadness=43 AllowBurrowing=false ; ; Just icy water. ; [TileSet0060] SetName = MM Water FileName = MWater TilesInSet = 14 LowRadarColor=10,10,80 HighRadarColor=15,15,110 NonMarbleMadness=21 ToTemperateTheater=69 AllowBurrowing=false [TileSet0061] SetName = Destroyable Cliffs FileName = dcliff TilesInSet = 2 Morphable = false LowRadarColor=120,120,150 HighRadarColor=120,120,150 ToTemperateTheater=56 AllowBurrowing=false MarbleMadness=74 [TileSet0062] SetName = Rock LAT FileName = Rock TilesInSet = 1 Morphable = false AllowBurrowing = false LowRadarColor = 10,90,90 HighRadarColor = 10,128,128 ToTemperateTheater=33 RequiredForRMG = true ; ; L.A.T. system for connecting rocky and normal terrain ; [TileSet0063] SetName = Rock/Clear LAT FileName = rlat TilesInSet = 16 Morphable = false AllowBurrowing = false LowRadarColor = 50,90,90 HighRadarColor = 70,128,128 AllowToPlace=no ToTemperateTheater=34 RequiredForRMG = true [TileSet0064] SetName = Grey FileName = Grey TilesInSet = 1 Morphable = false AllowBurrowing = false LowRadarColor = 10,100,10 HighRadarColor = 10,120,10 ToTemperateTheater=41 RequiredForRMG = true ; ; L.A.T. system for connecting grey and normal terrain ; [TileSet0065] SetName = Grey/Clear LAT FileName = glat TilesInSet = 16 Morphable = false AllowBurrowing = false LowRadarColor = 40,90,0 HighRadarColor = 80,110,0 AllowToPlace=no ToTemperateTheater=42 RequiredForRMG = true [TileSet0066] SetName = DirtTrackTunnel Floor FileName = dtunnt TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToTemperateTheater=72 AllowBurrowing=false MarbleMadness=71 [TileSet0067] SetName = DirtTunnel Floor FileName = dtunn TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToTemperateTheater=73 AllowBurrowing=false MarbleMadness=71 [TileSet0068] SetName = Pavement (Use for LAT) FileName = Pvclr TilesInSet=1 Morphable=no LowRadarColor = 128,128,128 HighRadarColor = 128,128,128 AllowBurrowing=false RequiredForRMG = true ToTemperateTheater=46 [TileSet0069] SetName = Pavement FileName = Pave TilesInSet = 14 Morphable = false LowRadarColor = 128,128,128 HighRadarColor = 128,128,128 AllowBurrowing=false RequiredForRMG = true ToTemperateTheater=38 ; ; L.A.T. system for connecting pavement and normal terrain ; [TileSet0070] SetName = Pavement/Clear LAT FileName = plat TilesInSet = 16 Morphable = false LowRadarColor = 110,80,40 HighRadarColor = 150,100,65 AllowToPlace=no AllowBurrowing=false RequiredForRMG = true ToTemperateTheater=39 ; ;New MarbleMadness set. ; ; MM Tunnel set [TileSet0071] SetName = MM Tunnel set FileName = mtunnl TilesInSet = 4 ShadowCaster = false Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 NonMarbleMadness=71 AllowBurrowing=false [TileSet0072] SetName = MM Tunnel Side FileName = mtunnx TilesInSet = 2 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 NonMarbleMadness=48 [TileSet0073] SetName = MM Monorail Slopes FileName = mtslop TilesInSet = 4 Morphable = no NonMarbleMadness = 45 LowRadarColor=92,92,92 HighRadarColor=92,92,92 AllowBurrowing=false [TileSet0074] SetName = MM Destroyable Cliffs FileName = mdclif TilesInSet = 2 Morphable = false LowRadarColor=120,120,150 HighRadarColor=120,120,150 NonMarbleMadness=61 AllowBurrowing=false [TileSet0075] ; MM ice shore ;Reserved for future use ; [TileSet0076] ; MM ice lat 1 ;Reserved for future use ; [TileSet0077] ; MM ice lat 2 ;Reserved for future use ; [TileSet0078] ; MM ice lat 3 ;Reserved for future use ; ; ; Animating tiles ; [Waterfalls] Tile01Anim=WA01X Tile01XOffset=5 Tile01YOffset=54 Tile01AttachesTo=0 Tile01ZAdjust=0 Tile02Anim=WA02X Tile02XOffset=-34 Tile02YOffset=41 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WA03X Tile03XOffset=-27 Tile03YOffset=48 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WA04X Tile04XOffset=-44 Tile04YOffset=39 Tile04AttachesTo=0 Tile04ZAdjust=0 [Waterfalls-B] Tile01Anim=WB01X Tile01XOffset=39 Tile01YOffset=38 Tile01AttachesTo=0 Tile01ZAdjust=0 Tile02Anim=WB02X Tile02XOffset=36 Tile02YOffset=43 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WB03X Tile03XOffset=26 Tile03YOffset=51 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WB04X Tile04XOffset=14 Tile04YOffset=54 Tile04AttachesTo=0 Tile04ZAdjust=0 [Waterfalls-C] Tile01Anim=WC01X Tile01XOffset=8 Tile01YOffset=16 Tile01AttachesTo=0 Tile01ZAdjust=0 Tile02Anim=WC02X Tile02XOffset=4 Tile02YOffset=-5 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WC03X Tile03XOffset=13 Tile03YOffset=1 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WC04X Tile04XOffset=-40 Tile04YOffset=-7 Tile04AttachesTo=1 Tile04ZAdjust=0 [Waterfalls-D] Tile01Anim=WD01X Tile01XOffset=-7 Tile01YOffset=-8 Tile01AttachesTo=1 Tile01ZAdjust=0 Tile02Anim=WD02X Tile02XOffset=-1 Tile02YOffset=-8 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WD03X Tile03XOffset=-13 Tile03YOffset=1 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WD04X Tile04XOffset=0 Tile04YOffset=17 Tile04AttachesTo=0 Tile04ZAdjust=0 [Tunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 [TrackTunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 [DirtTunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 [DirtTrackTunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 ================================================ FILE: DXMainClient/Resources/INI/sound.ini ================================================ ****SOUND.INI IS OBSOLETE**** The game no longer reads this file. Use SOUND01.INI instead. If you try to add any new sounds here, they won't be played in-game. ================================================ FILE: DXMainClient/Resources/INI/sound01.ini ================================================ ; SOUND.INI ; All sounds in the game are specified here. The sound must ; first be specified in the sound list section. Then the appropriate ; sound data should be specified for those sounds that differ from ; the default ratings. ; All sounds in the game are listed here (by name). [SoundList] 000=FIRSTRM1 ;Firestorm defense burning 001=FACBLD1 ; 002=ION1 ;Ion cannon strike 003=PLSECAN2 ; 004=METEOR1 ;LARGE METEOR 005=METEOR2 ;SMALL METEOR 006=METHIT1 ;METEOR HITS GROUND 007=ICBM1 ;BIG, HUGE, ICBM ROCKET 008=DEDMAN1 009=DEDMAN2 010=DEDMAN3 011=DEDMAN4 012=NOTIFY 013=GUN18 ;Civilian gun 014=SSPLASH1 ; 015=SSPLASH2 ; 016=SSPLASH3 ; 017=dsaping1 ;Deployable sensor array ping 018=DEDMAN5 019=ORCAUP1 ;ORCA TAKES OFF 020=ORCADWN1 ;ORCA LANDS 021=DROPUP1 ;DROPSHIP TAKES OFF 022=DROPDWN1 ;DROPSHIP LANDS 023=CRMBLE2 ;Building crumbling 024=HOVRMIS1 ;HOVERMLRS ROCKET FIRE 025=GLNCH4 ;RPG launch 026=REPAIR11 ;REPAIR VEHICLE 027=OBELPOWR ;obelisk 028=SQUISHY2 ;placeholder for real guy squish 029=SCOLD8 ;Scold sound 030=COMMUP1 ;COMMUNICATIONS CENTER GOES ONLINE 031=RADARDN1 ;placeholder for communication failure 032=PLACE2 ;PLACE A BUILDING DOWN 033=EXPNEW01 ; 034=EXPNEW02 ; 035=EXPNEW03 ; 036=EXPNEW04 ; 037=EXPNEW05 ; 038=EXPNEW06 ; 039=EXPNEW07 ; 040=EXPNEW08 ; 041=EXPNEW09 ; 042=EXPNEW10 ; 043=EXPNEW11 ; 044=EXPNEW12 ; 045=EXPNEW13 ; 046=EXPNEW14 ; 047=EXPNEW15 ; 048=CASHTURN ;placeholder for cashturn 049=CREDUP1 ;CREDIT POSITIVE 050=CREDDWN1 ;CREDIT NEGATIVE 051=GATEDWN1 ;Gate going down 052=GATEUP1 ;Gate going up 053=SUBDRIL1 ;Subterrian drill 054=SONIC4 ;Sonic weapon fire 055=OBELRAY1 ;Obelisk firing laser 056=120MMF ;Artillery sound 057=INFGUN3 ;Infantry gun 058=VICER1 ; 059=CHAINGN1 ;BUGGY FIRES GUN/APACHE FIRES GUNS 060=CYGUN1 ;CYBORG FIRES GUN 061=RKETINF1 ;ROCKET INFANTRY FIRE 062=JUMPJET1 ;JUMP JET FIRES GUN 063=ORCAMIS1 ;ORCA FIGHTER ATTACK 064=SAMSHOT1 ;SAM SITE MISSILE 065=TSGUN4 066=SILENCER ;Sniper gun 067=120mmx9 ;Artillery sound two-shooter 068=BLEEP1 ;Generic beep 069=CLICKY1 ;Generic click 070=CLOAK5 ;Cloaking sound 071=GAMEFRM1 ;Game forming 072=GOSTGUN1 ;Ghost talker 073=HEALER1 ;Healing units 074=MESSAGE1 ;Incomming message 075=ICECRAK1 076=ICECRAK2 077=ICECRAK3 078=MISL1 079=SCRIN5B 080=BIGGGUN1 081=SLVKGUN1 082=SQUISH6 083=SANDBAG1 084=FIEND1 085=FIEND2 086=EXPDIRT1 087=ELECTRO1 088=EXPNEW16 089=EXPNEW17 090=EXPNEW18 091=EXPNEW19 092=15-I000 ;Infantry reporting 093=15-I002 ;Unit ready! 094=15-I004 ;Awaiting order 095=15-I006 ;Sir? 096=15-I008 ;Sir, yes sir! 097=15-I010 ;Ready 098=15-I012 ;Yes sir 099=15-I014 ;Yes sir! 100=15-I016 ;Orders received 101=15-I018 ;Moving out 102=15-I020 ;Advancing 103=15-I022 ;On my way 104=15-I024 ;You got it 105=15-I026 ;No problem 106=15-I032 ;Ready for action 107=FLAMTNK1 ; Flame tank fire 108=RAILUSE5 ;Heavy mech railgun 109=15-I038 ;Standing by 110=15-I040 ;Yeah 111=15-I042 ;Orders? 112=15-I044 ;Load and clear 113=15-I046 ;I'm on it 114=15-I048 ;Sir! 115=15-I050 ;Good as done 116=15-I058 ;I'm taking heavy fire 117=15-I060 ;Move! Move! Move! 118=15-I064 ;MEDIC! 119=11-I000 ;Oxanna - Yes 120=11-I002 ;Direct me 121=11-I004 ;Awaiting orders 122=11-I006 ;I'm ready 123=11-I008 ;Of course 124=11-I010 ;Immediatly 125=11-I012 ;Yes! 126=11-I014 ;For Kane 127=11-I016 ;They will pay for this 128=11-I018 ;For the brotherhood 129=12-I000 ;Slavik 130=12-I002 131=12-I004 132=12-I006 133=12-I008 134=12-I010 135=12-I012 136=12-I014 137=12-I016 138=13-I000 ;Tratos 139=13-I002 140=13-I004 141=13-I006 142=13-I008 143=13-I010 144=13-I012 145=13-I014 146=13-I016 147=13-I018 148=13-I020 149=14-I000 ;Ghost stalker 150=14-I002 151=14-I004 152=WRONG1 ;Build queue full 153=14-I008 154=14-I010 155=14-I012 156=14-I014 157=14-I016 158=21-I000 ;Spy 159=21-I002 160=21-I004 161=KLAX1 ;Klaxon 162=27-I002 ;Unit deploy response 163=21-I010 164=21-I012 165=HUNTER2 166=21-I016 167=LASTUR1 168=21-I022 169=22-I000 ;Cyborg 170=22-I002 171=22-I006 172=22-I008 173=22-I010 174=22-I012 175=22-I014 176=22-I016 177=22-I018 178=22-I020 179=23-I000 ;Cyborg commando 180=23-I002 181=23-I004 182=23-I006 183=23-I008 184=23-I010 185=23-I012 186=23-I014 187=23-I016 188=23-I018 189=23-I020 190=23-I022 191=24-I000 ;Mutant hijacker 192=24-I002 193=24-I004 194=24-I006 195=24-I008 196=24-I010 197=24-I012 198=24-I014 199=24-I016 200=24-I018 201=24-I020 202=24-I022 203=24-I024 204=32-I000 ;Banshee 205=32-I002 206=32-I004 207=32-I006 208=32-I008 209=09-I000 210=09-I002 211=09-I004 212=09-I006 213=19-I000 ; Engineer 214=19-I002 215=19-I006 216=19-I010 217=19-I016 218=19-I018 219=10-I000 ;Umagon 220=10-I002 221=10-I004 222=10-I006 223=10-I016 224=10-I020 225=10-I022 226=10-I024 227=10-I026 228=10-I028 229=10-I030 230=DEDMAN6 231=DEDGIRL1 232=DEDGIRL2 233=DEDGIRL3 234=DEDGIRL4 235=20-I000 236=20-I004 237=20-I006 238=20-I008 239=20-I010 240=20-I012 241=20-I016 242=20-I018 243=20-I020 244=25-I000 245=25-I002 246=25-I004 247=25-I006 248=25-I012 249=25-I014 250=25-I016 251=25-I018 252=25-I022 253=25-I024 254=25-I026 255=30-I000 256=30-I002 257=30-I004 258=30-I006 259=30-I014 260=30-I016 261=30-I018 262=30-I022 263=30-I030 264=30-I034 265=30-I036 266=42-I000 ;Toxin soldier 267=42-I002 268=42-I004 269=42-I006 270=42-I008 271=42-I010 272=42-I012 273=60-N100 ; Firestorm additions start here 274=60-N102 275=60-N104 276=60-N106 277=60-N108 278=60-N110 279=60-N112 280=60-N114 281=60-N116 282=53-I000 283=53-I002 284=53-I004 285=53-I006 286=53-I008 287=53-I010 288=53-I012 289=54-N022 290=54-N024 291=54-N026 292=54-N028 293=54-N030 294=67-N100 295=67-N102 296=67-N104 297=67-N106 298=67-N108 299=68-N100 300=68-N102 301=68-N104 302=68-N106 303=68-N108 304=68-N110 305=69-N100 306=69-N102 307=69-N104 308=69-N106 309=69-N108 310=69-N110 311=70-N000 312=70-N002 313=70-N004 314=70-N006 315=70-N008 316=70-N010 317=70-N012 318=70-N014 319=70-N016 320=70-N018 321=COREFIR1 322=COREUP1 323=FIREWEB1 324=FLOATMOV 325=JUGGER1 326=LIMPBOM1 327=LIMPC3 328=LIMPQ3 329=SPIDDIE1 330=MOBEMP1 331=MSG1 332=OBELMOD1 333=22-N104 334=22-N106 335=22-N108 336=LIMPC4 337=LIMPQ4 338=FLOTMOV2 339=FLOTMOV3 340=FLOTMOV4 341=FLOATK1 342=OBELCOR3 999=BOOP ;Stub sound used for all unsigned sound trigers ; ******* Individual Sound Overrides ******* ; These sections are used to specify overrides from ; the default values for sound effects. ; ; Priority = Priority adjustment from normal (def=10) ; Increasing numbers have higher priority. ; Volume = volume to play sound at (def=1.0) [FIRSTRM1] [FACBLD1] [ION1] Priority=75 [PLSECAN2] [METEOR1] Priority=75 [METEOR2] Priority=75 [METHIT1] Priority=75 [ICBM1] Priority=75 [DEDMAN1] Priority=50 [DEDMAN2] Priority=50 [DEDMAN3] Priority=50 [DEDMAN4] Priority=50 [NOTIFY] [GUN18] [SSPLASH1] [SSPLASH2] [SSPLASH3] [DSAPING1] [DEDMAN5] Priority=50 [ORCAUP1] [ORCADWN1] [DROPUP1] [DROPDWN1] [CRMBLE2] [HOVRMIS1] [GLNCH4] [REPAIR11] [OBELPOWR] [SQUISHY2] Priority=50 [SCOLD8] [COMMUP1] [RADARDN1] [PLACE2] [EXPNEW01] Priority=50 [EXPNEW02] Priority=50 [EXPNEW03] Priority=50 [EXPNEW04] Priority=50 [EXPNEW05] Priority=50 [EXPNEW06] Priority=50 [EXPNEW07] Priority=50 [EXPNEW08] Priority=50 [EXPNEW09] Priority=50 [EXPNEW10] Priority=50 [EXPNEW11] Priority=50 [EXPNEW12] Priority=50 [EXPNEW13] Priority=50 [EXPNEW14] Priority=50 [EXPNEW15] Priority=50 [CASHTURN] [CREDUP1] Priority=15 [CREDDWN1] [GATEDWN1] [GATEUP1] [SUBDRIL1] [SONIC4] [OBELRAY1] [120MMF] [INFGUN3] [VICER1] [CHAINGN1] [CYGUN1] [RKETINF1] [JUMPJET1] [ORCAMIS1] [SAMSHOT1] [TSGUN4] [SILENCER] [120mmx9] [BLEEP1] [CLICKY1] [CLOAK5] [GAMEFRM1] [GOSTGUN1] [HEALER1] [MESSAGE1] Priority=15 [ICECRAK1] [ICECRAK2] [ICECRAK3] [MISL1] [SCRIN5B] [BIGGGUN1] [SLVKGUN1] [SQUISH6] Priority=55 [SANDBAG1] [FIEND1] [FIEND2] [LASTUR1] Priority=25 [EXPDIRT1] Priority=50 [ELECTRO1] Priority=50 [EXPNEW16] Priority=50 [EXPNEW17] Priority=50 [EXPNEW18] Priority=50 [EXPNEW19] Priority=50 [15-I000] Priority=100 [15-I002] Priority=100 [15-I004] Priority=100 [15-I006] Priority=100 [15-I008] Priority=100 [15-I010] Priority=100 [15-I012] Priority=100 [15-I014] Priority=100 [15-I016] Priority=100 [15-I018] Priority=100 [15-I020] Priority=100 [15-I022] Priority=100 [15-I024] Priority=100 [15-I026] Priority=100 [15-I032] Priority=100 [FLAMTNK1] [RAILUSE5] [15-I038] Priority=100 [15-I040] Priority=100 [15-I042] Priority=100 [15-I044] Priority=100 [15-I046] Priority=100 [15-I048] Priority=100 [15-I050] Priority=100 [15-I058] Priority=100 [15-I060] Priority=100 [15-I064] Priority=100 [11-I000] Priority=100 [11-I002] Priority=100 [11-I004] Priority=100 [11-I006] Priority=100 [11-I008] Priority=100 [11-I010] Priority=100 [11-I012] Priority=100 [11-I014] Priority=100 [11-I016] Priority=100 [11-I018] Priority=100 [12-I000] Priority=100 [12-I002] Priority=100 [12-I004] Priority=100 [12-I006] Priority=100 [12-I008] Priority=100 [12-I010] Priority=100 [12-I012] Priority=100 [12-I014] Priority=100 [12-I016] Priority=100 [13-I000] Priority=100 [13-I002] Priority=100 [13-I004] Priority=100 [13-I006] Priority=100 [13-I008] Priority=100 [13-I010] Priority=100 [13-I012] Priority=100 [13-I014] Priority=100 [13-I016] Priority=100 [13-I018] Priority=100 [13-I020] Priority=100 [14-I000] Priority=100 [14-I002] Priority=100 [14-I004] Priority=100 [WRONG1] [14-I008] Priority=100 [14-I010] Priority=100 [14-I012] Priority=100 [14-I014] Priority=100 [14-I016] Priority=100 [21-I000] Priority=100 [21-I002] Priority=100 [21-I004] Priority=100 [KLAX1] [27-I002] Priority=100 [21-I010] Priority=100 [21-I012] Priority=100 [HUNTER2] Priority=75 [21-I016] Priority=100 [21-I022] Priority=100 [22-I000] Priority=100 [22-I002] Priority=100 [22-I006] Priority=100 [22-I008] Priority=100 [22-I010] Priority=100 [22-I012] Priority=100 [22-I014] Priority=100 [22-I016] Priority=100 [22-I018] Priority=100 [22-I020] Priority=100 [23-I000] Priority=100 [23-I002] Priority=100 [23-I004] Priority=100 [23-I006] Priority=100 [23-I008] Priority=100 [23-I010] Priority=100 [23-I012] Priority=100 [23-I014] Priority=100 [23-I016] Priority=100 [23-I018] Priority=100 [23-I020] Priority=100 [23-I022] Priority=100 [24-I000] Priority=100 [24-I002] Priority=100 [24-I004] Priority=100 [24-I006] Priority=100 [24-I008] Priority=100 [24-I010] Priority=100 [24-I012] Priority=100 [24-I014] Priority=100 [24-I016] Priority=100 [24-I018] Priority=100 [24-I020] Priority=100 [24-I022] Priority=100 [24-I024] Priority=100 [32-I000] Priority=100 [32-I002] Priority=100 [32-I004] Priority=100 [32-I006] Priority=100 [32-I008] Priority=100 [09-I000] Priority=100 [09-I002] Priority=100 [09-I004] Priority=100 [09-I006] Priority=100 [19-I000] Priority=100 [19-I002] Priority=100 [19-I006] Priority=100 [19-I010] Priority=100 [19-I016] Priority=100 [19-I018] Priority=100 [10-I000] Priority=100 [10-I002] Priority=100 [10-I004] Priority=100 [10-I006] Priority=100 [10-I016] Priority=100 [10-I020] Priority=100 [10-I022] Priority=100 [10-I024] Priority=100 [10-I026] Priority=100 [10-I028] Priority=100 [10-I030] Priority=100 [DEDMAN6] Priority=50 [DEDGIRL1] Priority=50 [DEDGIRL2] Priority=50 [DEDGIRL3] Priority=50 [DEDGIRL4] Priority=50 [20-I000] Priority=100 [20-I004] Priority=100 [20-I006] Priority=100 [20-I008] Priority=100 [20-I010] Priority=100 [20-I012] Priority=100 [20-I016] Priority=100 [20-I018] Priority=100 [20-I020] Priority=100 [25-I000] Priority=100 [25-I002] Priority=100 [25-I004] Priority=100 [25-I006] Priority=100 [25-I012] Priority=100 [25-I014] Priority=100 [25-I016] Priority=100 [25-I018] Priority=100 [25-I022] Priority=100 [25-I024] Priority=100 [25-I026] Priority=100 [30-I000] Priority=100 [30-I002] Priority=100 [30-I004] Priority=100 [30-I006] Priority=100 [30-I014] Priority=100 [30-I016] Priority=100 [30-I018] Priority=100 [30-I022] Priority=100 [30-I030] Priority=100 [30-I034] Priority=100 [30-I036] Priority=100 [42-I000] Priority=100 [42-I002] Priority=100 [42-I004] Priority=100 [42-I006] Priority=100 [42-I008] Priority=100 [42-I010] Priority=100 [42-I012] Priority=100 ; Firestorm additions start here [60-N100] Priority=100 [60-N102] Priority=100 [60-N104] Priority=100 [60-N106] Priority=100 [60-N108] Priority=100 [60-N110] Priority=100 [60-N112] Priority=100 [60-N114] Priority=100 [60-N116] Priority=100 [53-I000] Priority=100 [53-I002] Priority=100 [53-I004] Priority=100 [53-I006] Priority=100 [53-I008] Priority=100 [53-I010] Priority=100 [53-I012] Priority=100 [54-N022] Priority=100 [54-N024] Priority=100 [54-N026] Priority=100 [54-N028] Priority=100 [54-N030] Priority=100 [67-N100] Priority=100 [67-N102] Priority=100 [67-N104] Priority=100 [67-N106] Priority=100 [67-N108] Priority=100 [68-N100] Priority=100 [68-N102] Priority=100 [68-N104] Priority=100 [68-N106] Priority=100 [68-N108] Priority=100 [68-N110] Priority=100 [69-N100] Priority=100 [69-N102] Priority=100 [69-N104] Priority=100 [69-N106] Priority=100 [69-N108] Priority=100 [69-N110] Priority=100 [70-N000] Priority=100 [70-N002] Priority=100 [70-N004] Priority=100 [70-N006] Priority=100 [70-N008] Priority=100 [70-N010] Priority=100 [70-N012] Priority=100 [70-N014] Priority=100 [70-N016] Priority=100 [70-N018] Priority=100 [COREFIR1] Priority=75 [COREUP1] Priority=100 [FIREWEB1] Priority=75 [FLOATMOV] Priority=50 [JUGGER1] Priority=75 [LIMPBOM1] Priority=100 [LIMPC3] Priority=100 [LIMPQ3] Priority=100 [SPIDDIE1] Priority=75 [MOBEMP1] Priority=75 [MSG1] Priority=100 [OBELMOD1] Priority=75 [22-N104] Priority=100 [22-N106] Priority=100 [22-N108] Priority=100 [LIMPC4] Priority=100 [LIMPQ4] Priority=100 [FLOTMOV2] Priority=30 [FLOTMOV3] Priority=30 [FLOTMOV4] Priority=30 [FLOATK1] Priority=30 [OBELCOR3] ================================================ FILE: DXMainClient/Resources/INI/temperat.ini ================================================ ;Modified July 30, 2002 ;Marble madness set added ;By DJBREIT : Version 1.0 ; ; ***Tiberian Sun Isometric Tile Control File*** ; ; ; General section. ; ; RampBase ; Number of tile set that includes all the ramp types ; ; MMRampBase ; Number of tile set that has the marble madness mode ramps ; ; ClearTile ; Number of tile set to use for clear terrain ; ; RoughTile ; Number of tile set that has the rough terrain ; ; ClearToRoughLAT ; Tile set that has the 16 tiles for the clear/rough LAT system ; ; HeightBase ; First tile of marble madness height tiles ; ; BlackTile ; Black tile used when rendering non-existent cells ; ; BridgeSet ; Tile set that contains bridge edges ; ; BridgeTopLeft1 ; BridgeTopLeft2 ; BridgeBottomRight1 ; BridgeBottomRight2 ; BridgeTopRight1 ; BridgeTopRight2 ; BridgeBottomLeft1 ; BridgeBottomLeft2 ; Tiles in bridge set to search for when fixing up bridges ; ; [General] RampBase = 9 RampSmooth = 43 MMRampBase = 7 ClearTile = 0 RoughTile = 13 ClearToRoughLat = 14 SandTile = 33 ClearToSandLat = 34 GreenTile = 41 ClearToGreenLat = 42 PaveTile = 46 MiscPaveTile = 38 ClearToPaveLat = 39 HeightBase = 52 BlackTile = 6 CliffSet = 10 ShorePieces = 12 WaterSet = 21 Ice1Set = 31 Ice2Set = 32 IceShoreSet = 32 BridgeSet = 19 TrainBridgeSet = 37 SlopeSetPieces = 25 SlopeSetPieces2 = 26 MonorailSlopes = 48 Tunnels = 53 TrackTunnels = 55 DirtTunnels = 72 DirtTrackTunnels = 73 WaterfallEast = 49 WaterfallWest = 51 WaterfallNorth = 50 WaterfallSouth = 30 CliffRamps = 25 PavedRoads = 20 PavedRoadEnds = 36 Medians = 40 RoughGround=35 DirtRoadJunction = 17 DirtRoadCurve = 16 DirtRoadStraight = 18 BridgeTopLeft1 = 1 BridgeTopLeft2 = 2 BridgeBottomRight1 = 3 BridgeBottomRight2 = 3 BridgeTopRight1 = 4 BridgeTopRight2 = 5 BridgeBottomLeft1 = 6 BridgeBottomLeft2 = 6 BridgeMiddle1 = 7 BridgeMiddle2 = 12 DestroyableCliffs = 56 WaterCliffs = 15 WaterCaves = 57 PavedRoadSlopes = 47 DirtRoadSlopes = 23 CrystalTile = 74 ClearToCrystalLat = 75 SwampTile = 76 WaterToSwampLat = 77 BlueMoldTile = 78 ClearToBlueMoldLat = 79 CrystalCliff = 80 ; ; TS Will scan through this file when loading up a theater and read in the ; isometric tile files specified. ; ; [TileSetnnnn] ; This is the tile set section header. TS will loop through from TileSet0000 ; upwards until it finds a set that hasnt been specified. ; ; SetName ; The name of the set as it will appear in the editor. ; ; FileName ; The base file name of each file in the set. The files in a set must all ; have the same basic name with a 2 digit id number appended. For example ; cliff01.tem, cliff02.tem, cliff03.tem. The 2 digit number starts at 01 ; not 00. ; ; TilesInSet ; The number of files comprising the set. There is a practical limit of ; 99 due to the 2 digit file name suffix. ; ; LastTilesInSet ; The number of tiles which the set used to have. This tells the ; game that the number of tiles in the set has changed and it should fix up ; the tile numbers when a map is loaded. If the map is then saved again, ; it will be saved with the correct tile numbers. This value should only ; be used to load up maps, convert the tile numbers, then save the maps ; out again. Then the LastTilesInSet entry should be removed or the newly ; fixed up maps will not load correctly. ; ; MarbleMadness ; The section number of the tile set to use for these tiles when in ; marble madness mode. ; ; NonMarbleMadness ; For marble madness tiles, this is the tile set to use when not in ; marble madness mode. ; ; Morphable ; Can this tile set be modified using the raise/lower ground function? ; ; ShadowCaster ; Do the tiles in this set cast shadows (cliff pieces) ; ; ToTemperateTheater ; The equivilent tile section in the temperate theater ; ; ToSnowTheater ; The equivilent tile section in the snow theater ; ; LowRadarColor ; What color to show on the radar for this set at the lowest height ; ; HighRadarColor ; What color to show on the radar for this set at the highest height ; ; AllowToPlace ; Should this tile be visible in the placement dialogue (def = true)? ; ; Blank tile for filling in holes. ; [TileSet0000] SetName = Clear FileName = Clear TilesInSet = 1 Morphable = true LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 RequiredForRMG = true AllowBurrowing = true AllowTiberium = true ; ; A few buildings ; [TileSet0001] SetName = Misc Buildings FileName = Bld TilesInSet = 3 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 AllowToPlace=no AllowBurrowing=false ; ; Some basic flat tiles ; [TileSet0002] SetName = Clear FileName = Snow TilesInSet = 4 Morphable = true LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 RequiredForRMG = true AllowTiberium = true ; ; A couple of old cliff pieces (not used) ; [TileSet0003] SetName = Cliff Pieces FileName = clif TilesInSet = 2 AllowToPlace=no AllowBurrowing=false ; ; A large ice flow. ; [TileSet0004] SetName = Ice Flow FileName = flow TilesInSet = 1 AllowBurrowing=false ; ; A nice little house. ; [TileSet0005] SetName = House FileName = house TilesInSet = 1 AllowToPlace=no AllowBurrowing=false ; ; Blank tile used for filling areas with no cell data. ; [TileSet0006] SetName = Blank FileName = blank TilesInSet = 1 Morphable = true LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 AllowToPlace=no AllowBurrowing=false RequiredForRMG = true AllowTiberium = true ; ; Marble madness mode ramp pieces. ; [TileSet0007] SetName = MM Ramps FileName = mslop TilesInSet = 20 NonMarbleMadness = 9 Morphable = true ;LastTilesInSet = 16 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 AllowTiberium = true ; ; Height pieces for marble madness mode. ; ; Obsolete. Replaced with HITE01 - HITE10 ; [TileSet0008] SetName = Obsolete Height Pieces FileName = hyte TilesInSet = 7 Morphable = true AllowToPlace=no AllowTiberium = true ; ; Misc theater ramps ; [TileSet0009] SetName = Ramps FileName = slope TilesInSet = 20 MarbleMadness = 7 Morphable = true ;LastTilesInSet = 16 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 RequiredForRMG = true AllowTiberium = true ; ; Cliff set. ; [TileSet0010] SetName = Cliff Set FileName = Cliff TilesInSet = 40 MarbleMadness = 22 ShadowCaster = true ShadowTiles = 40 LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 AllowBurrowing=false RequiredForRMG = true ; ; Civilian buildings ; [TileSet0011] SetName = Civilian Buildings FileName = Civ TilesInSet = 8 AllowToPlace=no AllowBurrowing=false ; ; Shore pieces ; [TileSet0012] SetName = Shore Pieces FileName = Shore TilesInSet = 42 LowRadarColor = 30,30,40 HighRadarColor = 30,30,40 MarbleMadness = 61 AllowBurrowing=false RequiredForRMG = true ; ; Clear terrain (slightly rough) ; [TileSet0013] SetName = Rough LAT tile FileName = Ruff TilesInSet = 1 Morphable = true LowRadarColor = 70,40,0 HighRadarColor = 80,45,0 RequiredForRMG = true AllowTiberium = true ; ; L.A.T. system for connecting clear and rough clear terrain ; [TileSet0014] SetName = Clear/Rough LAT FileName = clat TilesInSet = 16 Morphable = true LowRadarColor = 80,45,0 HighRadarColor = 100,60,0 AllowToPlace=no RequiredForRMG = true AllowTiberium = true ; ; Cliff pieces that meet water pieces ; [TileSet0015] SetName = Cliff/Water pieces FileName = WCliff TilesInSet = 28 ShadowCaster = true ShadowTiles = 28 LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 MarbleMadness=67 AllowBurrowing=false ; ; Dirt roads. Corner pieces. ; [TileSet0016] SetName = Bendy Dirt Roads FileName = Droadc TilesInSet = 24 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 MarbleMadness = 58 RequiredForRMG = true AllowTiberium = true ; ; Dirt roads. Junctions. ; [TileSet0017] SetName = Dirt Road Junctions FileName = Droadj TilesInSet = 11 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 MarbleMadness = 59 RequiredForRMG = true AllowTiberium = true ; ; Dirt roads. Straights. ; [TileSet0018] SetName = Straight Dirt Roads FileName = Droads TilesInSet = 66 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 MarbleMadness = 60 RequiredForRMG = true AllowTiberium = true ; ; Bridge sections. ; [TileSet0019] SetName = Bridges FileName = Ovrps TilesInSet = 16 LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false ; ; Paved roads. ; [TileSet0020] SetName = Paved Roads FileName = Proad TilesInSet = 21 LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false RequiredForRMG = true ; ; Just icy water. ; [TileSet0021] SetName = Water FileName = Water TilesInSet = 14 LowRadarColor = 10,10,30 HighRadarColor = 10,10,50 MarbleMadness=69 AllowBurrowing=false RequiredForRMG = true ; ; Cliff set. ; [TileSet0022] SetName = Marble Madness Cliff Set FileName = Mclif TilesInSet = 40 NonMarbleMadness = 10 ShadowCaster = true ShadowTiles = 40 LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 AllowBurrowing=false ; ; Dirt road slopes ; [TileSet0023] SetName = Dirt Road Slopes FileName = DRSLPE TilesInSet = 8 MarbleMadness = 24 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 RequiredForRMG = true AllowTiberium = true ; ; Marble Madness dirt road slopes ; [TileSet0024] SetName = MM Dirt Road Slopes FileName = MDRSLP TilesInSet = 8 NonMarbleMadness = 23 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 AllowTiberium = true ; ; Slope set pieces ; [TileSet0025] SetName = Slope Set Pieces FileName = RAMP TilesInSet = 10 MarbleMadness = 26 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 ShadowCaster = true ShadowTiles = 10 RequiredForRMG = true ; ; Slope set pieces - Marble Madness version ; [TileSet0026] SetName = Slope Set Pieces FileName = MRAM TilesInSet = 10 NonMarbleMadness = 25 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 ;ShadowCaster = true ;ShadowTiles = 10 ; ; A dead oil tanker ; [TileSet0027] SetName = Dead Oil Tanker FileName = TANKER TilesInSet = 1 AllowBurrowing=false ; ; Some ruins ; [TileSet0028] SetName = Ruins FileName = RUIN TilesInSet = 1 AllowBurrowing=false ; ; Height pieces for marble madness mode ; [TileSet0029] SetName = New MM Height Pieces FileName = hyte TilesInSet = 10 Morphable = true LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 AllowToPlace=no AllowTiberium = true ; ; Waterfalls. ; [TileSet0030] SetName = Waterfalls FileName = W-a- TilesInSet = 4 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 MarbleMadness=62 AllowBurrowing=false RequiredForRMG = true [TileSet0031] SetName = Ground 01 FileName = Des01 TilesInSet = 48 LowRadarColor = 10,80,80 HighRadarColor = 10,128,128 AllowTiberium = true [TileSet0032] SetName = Ground 02 FileName = Des02 TilesInSet = 48 LowRadarColor = 10,90,90 HighRadarColor = 10,128,128 AllowTiberium = true [TileSet0033] SetName = Sand FileName = Sandy TilesInSet = 1 Morphable = true LowRadarColor = 10,90,90 HighRadarColor = 10,128,128 ToSnowTheater=62 RequiredForRMG = true AllowTiberium = true ; ; L.A.T. system for connecting sandy and normal terrain ; [TileSet0034] SetName = Sand/Clear LAT FileName = dlat TilesInSet = 16 Morphable = true LowRadarColor = 50,90,90 HighRadarColor = 70,128,128 AllowToPlace=no ToSnowTheater=63 RequiredForRMG = true AllowTiberium = true [TileSet0035] SetName = Rough ground FileName = Rough TilesInSet = 10 Morphable = false ToSnowTheater = 40 LowRadarColor = 80,60,0 HighRadarColor = 110,80,0 RequiredForRMG = true [TileSet0036] SetName = Paved Road Ends FileName = p_end TilesInSet = 4 ToSnowTheater = 38 Morphable = false LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false RequiredForRMG = true [TileSet0037] SetName = TrainBridges FileName = Tovrps TilesInSet = 16 Morphable = false LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false [TileSet0038] SetName = Pavement FileName = Pave TilesInSet = 14 Morphable = false LowRadarColor = 128,128,128 HighRadarColor = 128,128,128 AllowBurrowing=false RequiredForRMG = true ToSnowTheater = 69 ; ; L.A.T. system for connecting pavement and normal terrain ; [TileSet0039] SetName = Pavement/Clear LAT FileName = plat TilesInSet = 16 Morphable = false LowRadarColor = 110,80,40 HighRadarColor = 150,100,65 AllowToPlace=no AllowBurrowing=false RequiredForRMG = true ToSnowTheater=70 [TileSet0040] SetName = Paved road bits FileName = proadc TilesInSet = 14 Morphable = false LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false RequiredForRMG = true [TileSet0041] SetName = Green FileName = Green TilesInSet = 1 Morphable = true LowRadarColor = 10,100,10 HighRadarColor = 10,120,10 ToSnowTheater=64 RequiredForRMG = true AllowTiberium = true ; ; L.A.T. system for connecting green and normal terrain ; [TileSet0042] SetName = Green/Clear LAT FileName = glat TilesInSet = 16 Morphable = true LowRadarColor = 40,90,0 HighRadarColor = 80,110,0 AllowToPlace=no ToSnowTheater=65 RequiredForRMG = true AllowTiberium = true [TileSet0043] SetName = Ramp edge fixup FileName = Rmpfx TilesInSet = 12 Morphable = true MarbleMadness = 44 ToSnowTheater = 41 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 RequiredForRMG = true AllowTiberium = true [TileSet0044] SetName = Ramp edge fixup - Marble Madness FileName = Mrmfx TilesInSet = 12 Morphable = true NonMarbleMadness = 43 ToSnowTheater = 42 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 AllowTiberium = true [TileSet0045] SetName = Water slopes FileName = WSLOPE TilesInSet = 4 Morphable = no ToSnowTheater = 43 LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 MarbleMadness=68 AllowBurrowing=false [TileSet0046] SetName = Pavement (Use for LAT) FileName = Pvclr TilesInSet=1 Morphable=no LowRadarColor = 128,128,128 HighRadarColor = 128,128,128 AllowBurrowing=false RequiredForRMG = true ToSnowTheater=68 [TileSet0047] SetName = Paved Road Slopes FileName = Prslpe TilesInSet = 4 Morphable = no ToSnowTheater = 44 LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false [TileSet0048] SetName = Monorail Slopes FileName = Tslope TilesInSet = 4 Morphable = no MarbleMadness = 85 ToSnowTheater = 45 LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false [TileSet0049] SetName = Waterfalls-B FileName = W-b- TilesInSet = 4 ToSnowTheater = 35 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 MarbleMadness=63 AllowBurrowing=false RequiredForRMG = true [TileSet0050] SetName = Waterfalls-C FileName = W-c- TilesInSet = 4 ToSnowTheater = 36 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 MarbleMadness=64 AllowBurrowing=false RequiredForRMG = true [TileSet0051] SetName = Waterfalls-D FileName = W-d- TilesInSet = 4 ToSnowTheater = 37 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 MarbleMadness=65 AllowBurrowing=false RequiredForRMG = true [TileSet0052] SetName = Newest MM Height FileName = hyte TilesInSet = 15 Morphable = true LowRadarColor=150,150,192 HighRadarColor=200,200,255 AllowToPlace=no AllowBurrowing=false AllowTiberium = true [TileSet0053] SetName = Tunnel Floor FileName = tunnel TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToSnowTheater=47 MarbleMadness=83 AllowBurrowing=false [TileSet0054] SetName = Tunnel Side FileName = tunnex TilesInSet = 2 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 MarbleMadness=84 ToSnowTheater=48 AllowBurrowing=false [TileSet0055] SetName = TrackTunnel Floor FileName = tunnet TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToSnowTheater=49 MarbleMadness=83 AllowBurrowing=false [TileSet0056] SetName = Destroyable Cliffs FileName = dcliff TilesInSet = 2 Morphable = false LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 ToSnowTheater=61 MarbleMadness=86 AllowBurrowing=false [TileSet0057] SetName = Water Caves FileName = Wcave TilesInSet = 8 Morphable = false LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 MarbleMadness=66 AllowBurrowing=false ; ; Dirt roads. Corner pieces. Marble Madness version. ; [TileSet0058] SetName = MM Bendy Dirt Roads FileName = MDrodc TilesInSet = 24 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness = 16 ToSnowTheater = 50 AllowTiberium = true ; ; Dirt roads. Junctions. Marble Madness version. ; [TileSet0059] SetName = MM Dirt Road Junctions FileName = MDrodj TilesInSet = 11 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness = 17 ToSnowTheater = 51 AllowTiberium = true ; ; Dirt roads. Straights. Marble Madness version. ; [TileSet0060] SetName = MM Straight Dirt Roads FileName = MDrods TilesInSet = 66 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness = 18 ToSnowTheater = 52 AllowTiberium = true ; ; Shore pieces. Marble madness version. ; [TileSet0061] SetName = MM Shore Pieces FileName = MShore TilesInSet = 42 LowRadarColor = 30,30,40 HighRadarColor = 30,30,40 NonMarbleMadness = 12 AllowBurrowing=false ; ; Waterfalls. MM. ; [TileSet0062] SetName = MM Waterfalls FileName = MWa- TilesInSet = 4 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 NonMarbleMadness=30 ToSnowTheater = 54 AllowBurrowing=false [TileSet0063] SetName = MM Waterfalls-B FileName = MWb- TilesInSet = 4 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 NonMarbleMadness=49 ToSnowTheater = 55 AllowBurrowing=false [TileSet0064] SetName = MM Waterfalls-C FileName = MWc- TilesInSet = 4 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 NonMarbleMadness=50 ToSnowTheater = 56 AllowBurrowing=false [TileSet0065] SetName = MM Waterfalls-D FileName = MWd- TilesInSet = 4 LowRadarColor = 128,128,192 HighRadarColor = 192,192,255 NonMarbleMadness=51 ToSnowTheater = 57 AllowBurrowing=false [TileSet0066] SetName = MM Water Caves FileName = MWcave TilesInSet = 8 Morphable = false LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 NonMarbleMadness=57 AllowBurrowing=false ; ; MM Cliff pieces that meet water pieces ; [TileSet0067] SetName = Cliff/Water pieces FileName = MWClif TilesInSet = 28 LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 NonMarbleMadness=15 ToSnowTheater=58 AllowBurrowing=false [TileSet0068] SetName = MM Water slopes FileName = MWSLOP TilesInSet = 4 Morphable = no LowRadarColor = 110,80,0 HighRadarColor = 150,110,0 NonMarbleMadness=45 ToSnowTheater = 59 AllowBurrowing=false ; ; Just icy water. ; [TileSet0069] SetName = MM Water FileName = MWater TilesInSet = 14 LowRadarColor = 10,10,30 HighRadarColor = 10,10,50 NonMarbleMadness=21 ToSnowTheater=60 AllowBurrowing=false [TileSet0070] SetName = Scrin Wreckage FileName = Scrin TilesInSet = 6 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 MarbleMadness=71 AllowBurrowing=false [TileSet0071] SetName = MM Scrin Wreckage FileName = MScrin TilesInSet = 6 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 NonMarbleMadness=70 AllowBurrowing=false [TileSet0072] SetName = DirtTrackTunnel Floor FileName = dtunnt TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToSnowTheater=66 MarbleMadness=83 AllowBurrowing=false [TileSet0073] SetName = DirtTunnel Floor FileName = dtunn TilesInSet = 4 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 ToSnowTheater=67 MarbleMadness=83 AllowBurrowing=false ; Crystal terrain [TileSet0074] SetName = Crystal LAT tile FileName = Crys TilesInSet = 1 Morphable = false LowRadarColor = 70,40,0 HighRadarColor = 80,45,0 RequiredForRMG = true AllowTiberium = false ; L.A.T. system for connecting crystal and clear terrain [TileSet0075] SetName = Clear Crystal LAT FileName = Cylat TilesInSet = 16 Morphable = false LowRadarColor = 80,45,0 HighRadarColor = 100,60,0 AllowToPlace = no RequiredForRMG = true AllowTiberium = false ; Swamp terrain [TileSet0076] SetName = Swampy FileName = Swamp TilesInSet = 9 Morphable = false LowRadarColor = 70,40,0 HighRadarColor = 80,45,0 RequiredForRMG = true AllowTiberium = false AllowBurrowing=false ; L.A.T. system for connecting swamp and water terrain [TileSet0077] SetName = Swampy LAT FileName = Slat TilesInSet = 16 Morphable = false LowRadarColor = 80,45,0 HighRadarColor = 100,60,0 AllowToPlace = no RequiredForRMG = true AllowTiberium = false AllowBurrowing=false ; Blue Mold terrain [TileSet0078] SetName = Blue Mold FileName = Blue TilesInSet = 1 Morphable = false LowRadarColor = 70,40,0 HighRadarColor = 80,45,0 RequiredForRMG = true AllowTiberium = false AllowBurrowing=false ; L.A.T. system for connecting crystal and clear terrain [TileSet0079] SetName = Blue Mold LAT FileName = Blat TilesInSet = 16 Morphable = false LowRadarColor = 80,45,0 HighRadarColor = 100,60,0 AllowToPlace = no RequiredForRMG = true AllowBurrowing=false AllowTiberium = false ; Crystal Cliffs [TileSet0080] SetName = Crystal Cliff FileName = CCliff TilesInSet = 6 Morphable = false ShadowCaster = false LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 MarbleMadness=82 AllowBurrowing=false RequiredForRMG = true ; Kodiak Crash [TileSet0081] SetName = Kodiak Crash FileName = Crash TilesInSet = 7 LowRadarColor = 60,40,0 HighRadarColor = 80,50,0 AllowBurrowing=false RequiredForRMG = false ; ;New MarbleMadness set. ; ; MM Crystal Cliff [TileSet0082] SetName = MM Crystal Cliff pieces FileName = MCClif TilesInSet = 6 Morphable = false ShadowCaster = false LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 NonMarbleMadness=80 AllowBurrowing=false ; MM Tunnel set [TileSet0083] SetName = MM Tunnel set FileName = mtunnl TilesInSet = 4 ShadowCaster = false Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 NonMarbleMadness=53 AllowBurrowing=false [TileSet0084] SetName = MM Tunnel Side FileName = mtunnx TilesInSet = 2 Morphable = false LowRadarColor=150,150,192 HighRadarColor=200,200,255 NonMarbleMadness=53 ToSnowTheater=48 [TileSet0085] SetName = MM Monorail Slopes FileName = Mtslop TilesInSet = 4 Morphable = no NonMarbleMadness = 48 ToSnowTheater = 45 LowRadarColor = 92,92,92 HighRadarColor = 92,92,92 AllowBurrowing=false [TileSet0086] SetName = MM Destroyable Cliffs FileName = Mdclif TilesInSet = 2 Morphable = false LowRadarColor = 90,65,0 HighRadarColor = 110,80,0 ToSnowTheater=61 NonMarbleMadness=56 AllowBurrowing=false ; ; Animating tiles ; [Tunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 [TrackTunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 [DirtTunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 [DirtTrackTunnel Floor] Tile01Anim=TUNTOP01 Tile01XOffset=-48 Tile01YOffset=-37 Tile01AttachesTo=2 Tile01ZAdjust=-10 Tile02Anim=TUNTOP02 Tile02XOffset=48 Tile02YOffset=-37 Tile02AttachesTo=10 Tile02ZAdjust=-10 Tile03Anim=TUNTOP03 Tile03XOffset=-2 Tile03YOffset=-13 Tile03AttachesTo=0 Tile03ZAdjust=-100 Tile04Anim=TUNTOP04 Tile04XOffset=0 Tile04YOffset=-13 Tile04AttachesTo=0 Tile04ZAdjust=-100 ;[Scrin Wreckage] ;Tile05Anim=UFO ;Tile05XOffset=12 ;Tile05YOffset=-10 ;Tile05AttachesTo=3 ;Tile05ZAdjust=-100 [Waterfalls] Tile01Anim=WA01X Tile01XOffset=-9 Tile01YOffset=54 Tile01AttachesTo=0 Tile01ZAdjust=0 Tile02Anim=WA02X Tile02XOffset=-39 Tile02YOffset=39 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WA03X Tile03XOffset=-26 Tile03YOffset=47 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WA04X Tile04XOffset=-38 Tile04YOffset=37 Tile04AttachesTo=0 Tile04ZAdjust=0 [Waterfalls-B] Tile01Anim=WB01X Tile01XOffset=30 Tile01YOffset=43 Tile01AttachesTo=0 Tile01ZAdjust=0 Tile02Anim=WB02X Tile02XOffset=43 Tile02YOffset=38 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WB03X Tile03XOffset=29 Tile03YOffset=49 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WB04X Tile04XOffset=9 Tile04YOffset=56 Tile04AttachesTo=0 Tile04ZAdjust=0 [Waterfalls-C] Tile01Anim=WC01X Tile01XOffset=-2 Tile01YOffset=19 Tile01AttachesTo=0 Tile01ZAdjust=0 Tile02Anim=WC02X Tile02XOffset=5 Tile02YOffset=-6 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WC03X Tile03XOffset=14 Tile03YOffset=1 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WC04X Tile04XOffset=-41 Tile04YOffset=-5 Tile04AttachesTo=1 Tile04ZAdjust=0 [Waterfalls-D] Tile01Anim=WD01X Tile01XOffset=-8 Tile01YOffset=-4 Tile01AttachesTo=1 Tile01ZAdjust=0 Tile02Anim=WD02X Tile02XOffset=-2 Tile02YOffset=-9 Tile02AttachesTo=0 Tile02ZAdjust=0 Tile03Anim=WD03X Tile03XOffset=-17 Tile03YOffset=-2 Tile03AttachesTo=0 Tile03ZAdjust=0 Tile04Anim=WD04X Tile04XOffset=2 Tile04YOffset=20 Tile04AttachesTo=0 Tile04ZAdjust=0 ================================================ FILE: DXMainClient/Resources/INI/theme.ini ================================================ ; THEME.INI ; Lists and controls the musical themes available in the game. ; ******* Theme Controls ******* ; Each theme is listed here. Even if the theme is not normally available ; in the play list, it still must be declared. [Themes] 00=INTRO ;Main Menu Theme 01=VALVES1B ;Valves 02=DUSKHOUR ;Dusk Hour 03=FLURRY ;Flurry 04=MUTANTS ;Mutants 05=APPROACH ;Approach 06=GLOOM ;Gloom 07=INFRARED ;Infrared 08=MADRAP ;Mad Rap 09=REDSKY ;Red Sky 10=STORM ;Ion Storm 11=TIMEBOMB ;Time Bomb 12=WHATLURK ;What Lurks 13=DEFENSE ;Defense 14=HEROISM ;Heroism 15=LONETROP ;Lone Troop 16=NODCRUSH ;Nod Crush 17=PHAROTEK ;Pharotek 18=SCOUT ;Scout 19=SCORE ;Score Screen 20=MAPS ;Map Selection 21=IONSTORM ;Ion Storm Ambient 22=FSMAP ;FS Map Selection 23=ELUSIVE ;Elusive 24=HACKER ;Hacker 25=INFILTRA ;Infiltration 26=LINKUP ;Link Up 27=KMACHINE ;Killing Machine 28=RAINNITE ;Rain in the night (Part 2) 29=SLAVESYS ;Slave to the system 30=FSMENU ;FS Main Menu Theme 31=DMACHINE ;Deploy Machines ; ******* Individual Theme Data ******* ; The following sections supply the information specific to ; each theme declared. ; ; Name = display name of the theme ; Length = length of the theme (in minutes) ; Normal = Is it available through the in-game theme play list (def=yes)? ; Scenario = the scenario when the theme becomes available (def=0) ; Side = which side [or sides] get to hear this theme ; Repeat = Does this theme always loop (def=no)? [INTRO] Name=Intro Length=3.27 Normal=no Repeat=yes [VALVES1B] Name=Valves Length=3.27 Scenario=1 [DUSKHOUR] Name=Dusk Hour Length=4.11 Scenario=1 Side=GDI [FLURRY] Name=Flurry Length=4.11 Scenario=1 [MUTANTS] Name=Mutants Length=4.11 Scenario=1 Side=GDI [APPROACH] Name=Approach Length=4.42 Scenario=1 [GLOOM] Name=Gloom Length=3.37 Scenario=1 [INFRARED] Name=Infrared Length=4.26 Scenario=1 [MADRAP] Name=Mad Rap Length=4.29 Scenario=1 [REDSKY] Name=Red Sky Length=2.22 Scenario=1 [STORM] Name=Ion Storm Length=4.14 Scenario=1 [TIMEBOMB] Name=Time Bomb Length=2.04 Scenario=1 [WHATLURK] Name=What Lurks Length=5.13 Scenario=1 [DEFENSE] Name=Defense Length=4.03 Scenario=1 Side=Nod [HEROISM] Name=Heroism Length=4.06 Scenario=1 [LONETROP] Name=Lone Troop Length=4.39 Scenario=1 Side=GDI [NODCRUSH] Name=Nod Crush Length=3.45 Scenario=1 Side=Nod [PHAROTEK] Name=Pharotek Length=4.38 Scenario=1 Side=Nod [SCOUT] Name=Scout Length=4.14 Scenario=1 [SCORE] Name=Score Screen Normal=no Repeat=yes [MAPS] Name=Map Selection Normal=no Repeat=yes [IONSTORM] Name=Ion Storm Ambient Normal=no Repeat=yes [FSMAP] Name=FS Map Selection Normal=no Repeat=yes [ELUSIVE] Name=Elusive Length=4.27 Scenario=1 [HACKER] Name=Hacker Length=4.02 Scenario=1 [INFILTRA] Name=Infiltration Length=4.20 Scenario=1 [LINKUP] Name=Link Up Length=3.17 Scenario=1 [KMACHINE] Name=Killing Machine Length=3.27 Scenario=1 [RAINNITE] Name=Rain in the night (Part 2) Length=3.59 Scenario=1 [SLAVESYS] Name=Slave to the system Length=2.36 Scenario=1 [FSMENU] Name=FS Menu Normal=no Repeat=yes [DMACHINE] Name=Deploy Machines Length=3.42 Scenario=1 ================================================ FILE: DXMainClient/Resources/INI/tutorial.ini ================================================ ;=============================================== ; Tutorial Text for Tiberian Sun and Firestorm ;=============================================== ; ;Generic Text ;------------ ;640x400 Max line length is... ;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ;640x480 Max line length is... ;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ;800x600 Max line length is... ;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ;------------------------- ; Tiberian Sun Tutorial ;------------------------- [Tutorial] 1=Harvest the Tiberium to the north. 2=Objective One: Destroy all of Hassan's elite guard. 3=Objective One: Capture Hassan's T.V. station to the east. 4=Objective One: Build a Tiberium Refinery and begin harvesting the Tiberium to the southeast. 5=Objective Two: Build a Barracks to create more infantry. 6=Objective Three: Destroy all Nod forces in the area. 7=Note that your power is getting low. To get more power, build more Power Plants. 8=If your Tiberium Refinery is full, build Tiberium Silos to store the excess Tiberium. 9=Objective Two: To get production online build a Tiberium Refinery. 10=Beware Tiberium is lethal to unprotected infantry. 11=Stand forward and be recognized! 12=Stand and Identify yourself in the name of Kane. 13=Sound the Alarm! Slavik's Forces are here! 14=Hassan Soldier: Hold them here! 15=The traitors are coming, destroy the bridge! 16=Objective Two: Destroy Hassan's elite guard base. 17=We've been touched by the spirit hand of Kane, and are ready to serve the technology of peace. 18=Down with Hassan!!! 19=Build more infantry to defend the base! 20=****ESTABLISHING BATTLEFIELD CONTROL****..........Standby! 21=Power levels are low. Construct more Power Plants. 22=****BATTLEFIELD CONTROL ESTABLISHED**** 23=To repair a bridge, send an engineer into the repair hut near the bridge base. 24=The temple has been discovered, NOW DESTROY the GDI trespassers. 25=Objective One: Locate the crashed UFO and retrieve Kane's artifacts from inside. 26=Sir! The Tacitus is gone. Vega's men must've grabbed it. 27=Objective Two: Retrieve the cargo from the train. 28=Pull over for inspection! 29=O.K.! You're clear to enter. 30=Nod ICBMs detected. To stop them, DESTROY the launchers. 31=Clear the zone for M.C.V. dropship deployment! 32=Base perimeter has been breached! 33=To repair a structure left-click on the wrench icon in the sidebar and left-click on the structure. 34=Laser Turrets! RUN FOR IT! 35=Objective One: Contact the mutants - try searching near the local hospital. 36=Objective Two: Clear both ends of the tunnel to the west. 37=Objective Three: Locate the research facility to the north. 38=Objective Four: Destroy the research facility. 39=Objective One: Locate the toxin trucks. 40=Objective Two: Escort the toxin trucks past the GDI checkpoint to the east. 41=Objective One: Locate the abandoned Nod base to the north. 42=Objective Two: Destroy the GDI base. 43=Use the Weed Eater units to harvest the Tiberium veins. 44=Objective One: Establish a base and build a Tiberium Waste Facility. 45=Objective Two: Destroy the GDI base. 46=Tiberium waste convoy approaching. 47=Convoy destroyed. 48=Firestorm perimeter deactivated. 49=Objective One: Deploy the ICBM launchers near the beacons. 50=First launcher deployed. 51=Second launcher deployed. 52=Third launcher deployed. Objective complete. 53=The Philadelphia has been destroyed! 54=Objective Two: Build the Temple of Nod. 55=Launcher destroyed. 56=Mission failed. 57=Objective One: Infiltrate the GDI Communication Upgrade Centers. 58=Codes located. 59=Proceed to the next Communication Upgrade Center. 60=Objective Two: Evacuate the Chameleon Spy. 61=Supplies found. 62=Objective One: Establish a base. 63=Deploy the M.C.V. by double left clicking on the M.C.V. 64=Objective Two: Destroy all five Nod SAM sites. 65=Objective Reached: Civilians evacuated. 66=Objective Three: Destroy all Nod forces. 67=Objective One: Locate and secure the crash site. 68=Objective Two: Capture Nod Technology Center. 69=Objective Reached: Site secure 70=Objective Reached: Technology Center captured. 71=Warning: Mission critical unit under attack. 72=Warning: Mission critical structure under attack. 73=Objective One: Locate and free the mutants. 74=Objective Two: Evacuate the mutants. 75=Objective Reached: Mutants freed. 76=Objective One: Destroy the supply base. 77=Objective One: Plant C4 on all ten Nod power plants. 78=Objective One: Destroy Nod missile complex. 79=Detonate C4 when ready. 80=Stop! Don't Shoot! I was forced to work for them. 81=Take out this sentry post and I will show you their nearby base. 82=CABAL: Hassan's Base has been alerted. Attack is imminent. 83=Now get an engineer over here to fix this bridge and I 84=will alert Hassan to their presence. 85=CABAL: Establish a foothold on the far side of this bridge 86=and an MCV will be sent in to you. 87=CABAL: MCV has arrived to the southeast. 88=To build or train left-click on the icons located in the sidebar. 89=To deploy a vehicle select it, place the cursor over the vehicle and left-click on it. 90=GDI has detected you. 91=The Temple is under attack! 92=Mutant vermin detected. 93=Tiberium lifeform detected. 94=Objective One: Locate the old Temple of Nod. 95=Objective Two: Remove the GDI trespassers. 96=Tacitus has been acquired. 97=GDI dropship detected. 98=Bullet train departing. 99=Prevent the train from departing and retrieve the Tacitus. 100=GDI bullet train arriving. 101=GDI bullet train departing. 102=Transport has arrived. 103=Transport lost. 104=Objective One: Find and rescue Oxanna. 105=Objective Two: Commandeer a transport to escape. 106=CAUTION: THIN ICE. 107=Kodiak under attack! 108=Storm abating. Commence attack on Nod forces. 109=Kodiak destroyed! 110=Kodiak in critical condition! 111=Eye of the storm has been entered. 112=Maximum efficiency for equipment can now be achieved. 113=Reentering ion storm, caution is advised. 114=Objective One: Protect the Kodiak at all costs. 115=Objective Two: Destroy all Nod forces. 116=Philadelphia in range. 117=ICBM launch detected. 118=Tiberium Missile launched. 119=ICBM destroyed! 120=Objective One: Stop the launch of the Tiberium Missile. 121=Objective Two: Destroy the ICBMs targeted at the Philadelphia. 122=ICBMs destroyed! Philadelphia out of danger. 123=Proceed with Tiberium Missile destruction. 124=Objective Three: Destroy all Nod forces. 125=Civilian city is under attack! 126=PEACE THROUGH POWER! 127=****BROADCASTING**** 128=This map is under redesign. 129=GDI Soldier: Shit, We're outnumbered! Return to base now and alert them. 130=CABAL: During the Ion Storm their Radar/Communications will be down. Now is the opportune time to hit them before the 131=storm abates. 132=CABAL: GDI Communications have been reestablished. 133=They are sending a transmission to Sarajevo now. 134=Your venture has been quite unsuccessful, to state the least. 135=Move quickly, before they see us. 136=We have to get this to Tratos immediately. 137=Holy $#!+ its Nod! I have to warn the base. 138=What's the E.T.A. on that M.C.V.? This UFO gives me the creeps. 139=Looks like they're going to ship it out via bullet train. 140=Current weapon range insufficient. Weapon drop in progress. 141=Thanks for the help! 142=Power overload in progress... 143=GDI forces spotted. Blow the bridge! 144=Objective one: Destroy all Nod structures. 145=Objective two: Capture the train station. DO NOT DESTROY IT! 146=Objective one complete. 147=Nod base is heavily guarded by lasers. Suggestion: destroying power plants to west may cause overload. 148=Objective two complete. 149=Umagon: My people are nearby. 150=We will help. 151=Alert! GDI presence detected! 152=Perimeter secure. Deactivating alarm. 153=Alert! Prison break in progress! 154=Orca Transport: Negative on extraction until SAM sites are eliminated! 155=GDI Forces Spotted! Falling back to alert base. 156=Objective: Rescue captives from the prison to the east. 157=Objective one: Spy on GDI comm center to learn the location of the weapons test. 158=Objective two: Destroy the Mammoth Mark II prototype. 159=GDI: The MM2 is quite effective against structures. 160=GDI: Now watch the effectiveness against ground units. 161=GDI: The MM2 is equally deadly to air-based assaults. 162=GDI: This concludes the Mammoth Mark II demonstration. 163=We have Hassan pinned and ready to be brought in Commander Slavick. Orders are complete. 164=Sir! I believe there is an old GDI base near. It could be worth looking into. 165=We should rendezvous with the rescue team to the south. 166=UFO crash sight located. 167=Hey... over here! Help... Destroy these trucks to free us. 168=Captured Commander: All right! Now get me to your drop-off site and into the evac unit. 169=STOP THAT TRAIN! 170=Ghost Stalker: If you can get me onto that train, we can do some real damage! 171=Mutants: The charges are placed. We can get the laser wall down in 30 minutes. 172=Mutants: The wall is down - you are clear to attack! 173=Objective one: Destroy all the chemical tanks. 174=Objective two: Destroy the Nod base. 175=Objective one: Destroy all chemical missile launch sites. 176=New secondary objective: Destroy primary AND secondary Nod bases. 177=Oxanna located. 178=GDI: Jake, it's a trap! Get to the airbase! 179=GDI: Jake, the transport will take 30 minutes to arrive. Hold on! 180=GDI: Patrol to base! Nod troops in area! Abort tour! 181=New Objective: Get Ghost Stalker onto the train. Ghost must not die! 182=Nod: Commander, you have been provided with a direct satellite uplink for this mission. 183=Nod: Look to your radar now and you will see the three locations of the mobile sensor arrays. 184=You have been provided with 2 Artillery units. Good hunting, reinforcements will be arriving soon... 185=Oxanna is being moved to the main GDI base. 186=Nod: Umagon has escaped. Your mission has failed. 187=Nod: Umagon has been detected in the northeast quadrant. 188=She is boarding a train bound for the GDI base in the south. 189=Nod: Umagon has reached the GDI base and is moving to board the train leaving this region. 190=GDI: We've lost the beacon. Extraction time will be delayed 15 minutes. 191=Objective One: Capture the GDI base before McNeil arrives. 192=Objective Two: Use Toxin Soldiers to "convince" McNeil to join us. 193=Objective Three: Get McNeil into the APC at the extraction point. 194=Our cover is blown! Capture McNeil by any means possible! 195=GDI is going after our extraction APC It must not be destroyed! 196=Thanks! We can use the supplies. I'll go gather my people. 197=Nod: All sensor arrays are down. Full area map generation downloading now. 198=Special objective complete. 199=Nod: We can use these old units to our advantage. Rerouting their control to you in 3, 200=2, 201=1... 202=Hey! Where'd all those shiners come from? 203=Nod: Umagon's dropship transport has been located and will arrive in 10 minutes. 204=Nod: Umagon's dropship transport will arrive in 5 minutes. 205=Nod: Umagon's dropship transport will arrive in 1 minute. 206=Nod: Umagon's dropship transport has arrived and she is moving to board the southern train. 207=Nod: Umagon is moving to board the northern train which leaves the region. Her escape is imminent. 208=Cabal: Find and capture the train station before Umagon arrives. If she manages to make it onto a train then destroy it before she can escape. 209=GDI: Hurry Jake! They're right behind you! 210=GDI: Jake, it's good to see...Hey! What are you doing? 211=Two launchers remaining. 212=One launcher remaining. 213=Mutants: Liars! GDI is trying to help us! You will die for this! 214=Umagon: My people are waiting somewhere to the north. 215= SCROOGE! 216=Objective One: Capture the remaining GDI structures within this base to build a force to capture Tratos. 217=Objective Two: Now find the Mutant Headquarters and knock on their door (attack it!). This should convince Tratos to be sympathetic to our cause. 218=The Philadelphia is passing within ICBM Range. 219=The Philadelphia has left ICBM range. 220=Destroy the 7 SAM sites on the ridge to clear the way for our dropships. 221=Cabal: General Vega, the secondary generators will come online in 20 minutes. 222=Cabal: General Vega, the generators are online. SAM sites active. 223=EVA: We are currently tracking the Nod train carrying the target cargo. Intel states that the bridge is out 224=and we may hit the train before they repair the bridge. 225=EVA: Alert! The bridge has been fixed and the Nod train is moving to its final destination within the base 226=to the South. Penetrate the bases defenses and retrieve that cargo. 227=Objective one: Remove all Nod presence from the area. 228=Objective two: Capture Vega's Pyramid. 229=EVA: GDI reinforcements have arrived. Mammoth Mk II en route. Estimated ETA in 2 minutes... 230=EVA: Mammoth Mk II has arrived. 231=CABAL: Philadelphia orbit tracking commencing! 232=Mutants: Hold a moment, while their fighters pass by. 233=Mutants: Okay, Go now. 234=Mutants: The production facility has been located. Send in the reinforcements and let's finish this. 235=Mutants: Damn, their base has been cloaked. We must wait for them to uncloak it. 236=EVA: The cargo car of that train contains the crate of crystals that you are to recover. 237=EVA: The bridge has been repaired and the train is making it's way to the Nod base in the south. 238=EVA: Penetrate their base, destroy that cargo car and retrieve the crate holding the crystals. 239=EVA: Umagon lost, mission failed. 240=EVA: Ghost stalker lost, mission failed. 241=EVA: Mcneil lost, mission failed. 242=CABAL: Slavik lost, mission failed. 243=EVA: The crystals have been retrieved, mission complete. 244=CABAL: With the train destroyed Umagon will be stranded. Find her and capture her. 245=Tratos: Fight them my children, for the fate of our people. 246=Tratos: You have killed enough of my children, take me and be done with this violence. 247=Solomon: Change of plans - We have verified Vega's presence in the pyramid. CAPTURE the pyramid with Vega alive. DO NOT DESTROY IT! ;---------------------- ; Firestorm Tutorial ;---------------------- 248=A piece of CABAL's core has been recovered. 249=GDI has detected your presence. 250=GDI forces are near! 251=First Objective: Get an Engineer into the Temple of Nod to retrieve part of CABAL's core. 252=Second Objective: Retrieve a section of CABAL's core from the storage yard. 253=Third Objective: Prevent transportation of the last section of the core. 254=GDI is moving a piece of the core. 255=Intercept the core piece before it is transported. 256=Stop the cargo truck and retrieve the core piece. 257=Get to the pick up zone for immediate evacuation! 258= 259=First Objective: Remain hidden from the GDI forces in the area. 260=Second Objective: "Persuade" the civilians to assist our goal with the Toxin Soldiers. 261=Third Objective: Destroy all GDI and civilian structures in the region. 262=Use the Toxin soldiers to capture civilians. 263=To bait the Tiberium life forms, lure them out with the drugged civilians. 264=Lead the Tiberium life forms to the GDI/civilian occupied area. 265=Once the life form has snacked on the civilian pawns it will feast on the settlements. 266=First Objective: Find and evacuate any civilians in the area. 267=Second Objective: Maintain ALL factories until reinforcements arrive. 268=Escort the civilians to the ORCA transports for an immediate airlift. 271=Mutant Guard: Halt, and prepare for vehicle inspection! 272=Mutant Guard: Okay, looks good. Head on in. 273=CABAL: SAM sites destroyed. Air power incoming. 274=CABAL: You have been detected, Tratos is escaping by air transport. 275=CABAL: You have failed, Tratos has escaped. 276=CABAL: Arrays have been destroyed, sensors are now down. 277=First Objective: Find the Kodiak. 278=Second Objective: Determine if the Kodiak can be salvaged. 279=Third Objective: Return the Tacitus to the drop zone beacon. 280=EVA: Nod has acquired the Tacitus. Recover it and return it to the drop zone. 281=First Objective: Neutralize (do not kill) the four riot leaders. 282=Second Objective: Protect food and water processors at all costs. 283=Warning: Do not kill civilians or mutants! 284=Warning: Prevent the destruction of mutant and civilian structures. 285=First Objective: Get to the GDI outpost. 286=Second Objective: Get Dr. Boudreau to the landing pad. 287=Third Objective: Destroy all of CABAL's forces. 288=First Objective: Locate the Mutant base. 289=Second Objective: Return the truck containing the Tacitus to the beacon. 290=Third Objective: Destroy all Mutant forces. 291=Mutant: We've got you now Nod scum! 292=First Objective: Locate the abandoned airfield. 293=Second Objective: Repair the array. 294=Third Objective: Retreat to the Montauk. 295=Mutant Guard: It's bugged... Destroy it now! 296=Tacitus recovered and loaded on the truck. Proceed to the beacon. 297=Use the truck to transport the Tacitus when you locate it. 298=Thanks! We saw something crash to the east. We also spotted a Nod MCV. Be careful. 299=CABAL: The capture of 6 power plants will shut down the Firestorm Generator. 300=GDI Soldier: Sir, these laser posts are stronger than normal. 301=EVA: Locating fence technicians may help in shutting down the laser fencing. Their probable location is within a civilian outpost to the north. 302=Technicians: We can shut that fencing down for you, just get us into one of the fence power arrays. The first one is across the water. 303=EVA: Enemy Bridges may allow for unit reinforcement. Their destruction would be beneficial to this mission. 304=CABAL on-line... 305=EVA: Proceed to the base and secure it from attack. 306=Riot leader neutralized. 307=The serum within the tranquilizers they use will make them more accommodating to our plans. 308=The life forms are located in a tiberium accelerated staging area called the Genesis Pit. 309=CIVILIAN CASUALTY TOO HIGH! 310=The orders were clear commander - NO DEATHS! 311=Stop the patrol from alerting the base! 312=Use the riot troops to force civilians and mutants to retreat. 313=Use the Mobile EM-Pulse tanks to stop any mutant vehicles. 314=Too many mutant structures have been lost. 315=Too many civilian structures have been lost. 316=Cyborg Commando online. Retaliation protocols initiated. 317=Nod: CABAL forces are attacking! Evacuate the base! 318=Zealot: Look! A Mutant! 319=Zealot: Mutant Abomination! How dare you defile the 320= sanctity of our Holy ground? 321=Priest: Kill the Mutant! 322=Priest: STOP! THIEF! 323=Zealot: Kill the Heretics! 324=Zealot: Wha?! They've killed the Leader! 325=Zealot: Existence is futile! 326=Zealot: I'm coming to join you! 327=Welcome stranger! Surely the divine one 328=has guided your footsteps to this Holy Land. 329=Can I offer you a tasty beverage? 330="Temple of the Tacitus" 331=Archaeologist: Command, this is Valdez, I've got the Tacitus! 332=GDI Command: Roger that Valdez, Transport is dusting 333=off now. Extraction in T minus two minutes. 334=Join us! 335="Temple of Time" 336=Cult Member: Welcome Traveler! Have you come to 337=rejoice in the glory of our savior... 338=Jebediah Smith? 339=GDI Commander: This must be the base... 340=GDI Commander: What the...!? 341=GDI Commander: My God! CABAL is taking prisoners? 342=This can't be good. 343=GDI Commander: Arm Yourselves! CABAL is conscripting 344=humans into his Cyborg army. 345=Villager: Thanks for the warning. Here's 346=a reward for your concern. 347=GDI Commander: Be warned! CABAL has set up 348=operations in this sector and is capturing 349=humans to turn them into Cyborgs. 350=Villager: My God! Men arm yourselves! 351=Women and children to the shelter! 352=Here Commander, please take these two DISRUPTERS 353=to help in your battle. 354=GDI Commander: Attention Mutants! CABAL is 355=currently harvesting biological components 356=for his Cyborgs. Arm Yourselves! 357=Mutant: Understood Blunt! Take this 358=HARVESTER for your troubles. 359=GDI Commander: People of Trondheim, you must 360=evacuate the city immediately! CABAL is 361=actively capturing civilians to turn them 362=into Cyborgs. 363=Mayor: Yes, yes. I will see to it that 364=everyone is evacuated. Please take this 365=MCV and good luck. 366=Civilian: MAYDAY! MAYDAY! We are currently under siege. 367=Can anyone help us? 368=Nod General: Commander, GDI command has requested 369=that we aid these Civilians. We cannot refuse. 370=Rescue the plebes then take care of those harvesters. 371=Nod General: Forget the Civilians, they are 372=all dead. Concentrate on those harvesters. 373=Nod General: Well done, Commander. GDI Command 374=is so pleased that they have consented to send 375=you a MCV. 376=Nod Soldier: This should be easy enough. 377=Nod Soldier: Let's get those harvesters! 378=Archaeologist: The Hieroglyphs on this temple read: 379=Priest: Kill the MUTANT invader! 380="Temple of Thunder" 381=Use the ARCHAEOLOGIST to retrieve the Tacitus. 382=ARCHAEOLOGIST killed! 383=Get your people to the evacuation zone. 384=Aircraft approaching. 385=Priest: Praise this plant! 386=Priest: Mortimer predicted the coming of these creatures. 387= 388=Priest: Blessed are the beasts! 389=First Objective: Use your commander to warn the 390=CIVILIANS. 391=Second Objective: Destroy CABAL's base. 392=Use your ENGINEER to steal an EVA unit from GDI's RADAR FACILITY. 393=First Objective: Destroy all of CABAL's HARVESTERS, 394=REFINERIES, and SILOS. 395=Second Objective: Rescue the CIVILIANS. 396=GDI Command: We are providing you with a new unit, 397=the JUGGERNAUT. Do not waste it. 398=Get your ENGINEER to the evacuation zone. 399=Nod General: Well done, Commander. GDI Command 400=is so pleased that they have consented to send 401=you more funding. 402=Nod General: Well done, Commander. Reinforcements 403=en route. 404=I am the power and the glory! 405=Civilian: HELP! We are under attack! 406=Montauk Destroyed! 407=First Objective: Get the infected Cyborg into the communications network. 408=Second Objective: Destroy the cyborg production plant. 409=First Objective: Attach limpet mines to GDI units to penetrate the base and locate Tratos. 410=Second Objective: Deactivate the firestorm defenses and neutralize the sensor arrays. 411=Third Objective: Assassinate Tratos. 412=Cabal: Power plant eliminated. 5 left to capture or destroy. 413=Cabal: Power plant eliminated. 4 left to capture or destroy. 414=Cabal: Power plant eliminated. 3 left to capture or destroy. 415=Cabal: Power plant eliminated. 2 left to capture or destroy. 416=Cabal: Power plant eliminated. 1 left to capture or destroy. 417=To gain access to the Genesis Pit repair the bridge to the north of your location. 418=Repair the bridge to gain access to the Genesis Pit. ; Demo maps tutorial 419=Click on your units to select them. 420=Once selected, left-click where you want the unit(s) to move. 421=You can unselect the selected unit(s) by right-clicking. 422=As you move, you reveal terrain, structures, and enemy units. 423=To attack, select your unit(s), then left-click on the enemy unit or structure. 424=If you left-click and hold, you can make a band box to select many units at once. 425=Explore the city and destroy all Nod units. 426=Tiberium is a strange mineral that is collected and refined for money. 427=Destroy this Nod outpost to win the mission. 428=Every unit has different abilities. These MLRS can cross water. 429=Crates contain power-ups and other bonuses. 430=Bridges can be destroyed and repaired. 431=To repair a bridge select an engineer, and send it into the bridge hut. 432=Excellent work, Commander! 433=GDI has already established the basics of your base for you. 434=To build structures or units, left-click on their pictures on the sidebar. 435=A barracks will allow you to train infantry-type units. 436=Power plants will increase the amount of power in your base. 437=A Tiberium Refinery allows you to collect Tiberium and make more money. 438=The War Factory lets you produce vehicle-type units. 439=Radar allows you to see a map of the battlefield, including enemy objects. 440=When a structure is shows READY, click on it, then place it on the map. 441=To make money, build a Tiberium refinery. 442=The harvester will automatically harvest the nearest patch of Tiberium. 443=To keep power up, build more power plants. 444=Find the Nod base, and destroy all structures to win. 445=Silos hold extra money so you don't lose any when the refinery is full. 446=Vulcan cannons must be placed on empty component towers to work. 447=Component towers are used to place fixed defenses in your base. 448=Veinhole monsters can attack any non-infantry unit that touches their veins. 449=Tiberium damages infantry-type units as they cross it. Keep them away if possible. 450=Some cliffs are destroyable. Attack them to create a ramp. 451=To repair, left-click on the repair wrench then the damaged structure. ================================================ FILE: DXMainClient/Resources/Map Editor/test.txt ================================================ ================================================ FILE: DXMainClient/Resources/Maps/Custom/custom maps.txt ================================================ Any custom maps you made or downloaded are to be placed in this directory. Custom maps need to have a ".map" extension and they can be selected in the game by selecting the "Custom Map" game mode. ================================================ FILE: DXMainClient/Resources/SUN.ini ================================================ [Audio] PlayMainMenuMusic=False [Options] IsFirstRun=False CheckforUpdates=False WriteInstallationPathToRegistry=False PrivacyPolicyAccepted=True [MultiPlayer] DiscordIntegration=False [Video] ClientResolutionX=1280 ClientResolutionY=720 BorderlessWindowedClient=False IntegerScaledClient=True ================================================ FILE: DXMainClient/Startup.cs ================================================ using System; using System.IO; using System.Threading; using Microsoft.Win32; using DTAClient.Domain; using ClientCore; using Rampastring.Tools; using DTAClient.DXGUI; using ClientUpdater; using System.Security.Principal; using System.DirectoryServices; using System.Linq; using DTAClient.Online; using ClientCore.INIProcessing; using ClientCore.Enums; using System.Threading.Tasks; using System.Globalization; using System.Management; using System.Runtime.InteropServices; using System.Runtime.Versioning; using ClientCore.Settings; using ClientGUI; using Steamworks; namespace DTAClient { /// /// A class that handles initialization of the Client. /// public class Startup { /// /// The main method for startup and initialization. /// public void Execute() { ProgramConstants.RESOURCES_DIR = SafePath.CombineDirectoryPath(ProgramConstants.BASE_RESOURCE_PATH, UserINISettings.Instance.ThemeFolderPath); DirectoryInfo resourcesDirectory = SafePath.GetDirectory(ProgramConstants.GetResourcePath()); if (!resourcesDirectory.Exists) throw new DirectoryNotFoundException("Theme directory not found!" + Environment.NewLine + ProgramConstants.RESOURCES_DIR); Logger.Log("Initializing updater."); SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "version_u"); Updater.Initialize(ProgramConstants.GamePath, ProgramConstants.GetBaseResourcePath(), ClientConfiguration.Instance.SettingsIniName, ClientConfiguration.Instance.LocalGame, SafePath.GetFile(ProgramConstants.StartupExecutable).Name); Logger.Log("OSDescription: " + RuntimeInformation.OSDescription); Logger.Log("OSArchitecture: " + RuntimeInformation.OSArchitecture); Logger.Log("ProcessArchitecture: " + RuntimeInformation.ProcessArchitecture); Logger.Log("FrameworkDescription: " + RuntimeInformation.FrameworkDescription); Logger.Log("Selected OS profile: " + MainClientConstants.OSId); Logger.Log("Current culture: " + CultureInfo.CurrentCulture); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // The query in CheckSystemSpecifications takes lots of time, // so we'll do it in a separate thread to make startup faster Thread thread = new Thread(CheckSystemSpecifications); thread.Start(); } // Using tasks here causes crashes on Wine for some reason Thread onlineIdThread = new Thread(GenerateOnlineId); onlineIdThread.Start(); if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares) Task.Run(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, "debug"), DateTime.Now.AddDays(-7))); Task.Run(MigrateOldLogFiles); // Start INI file preprocessor PreprocessorBackgroundTask.Instance.Run(); DirectoryInfo updaterFolder = SafePath.GetDirectory(ProgramConstants.GamePath, "Updater"); if (updaterFolder.Exists) { Logger.Log("Attempting to delete temporary updater directory."); try { updaterFolder.Delete(true); } catch { } } if (ClientConfiguration.Instance.CreateSavedGamesDirectory) { DirectoryInfo savedGamesFolder = SafePath.GetDirectory(ProgramConstants.GamePath, "Saved Games"); if (!savedGamesFolder.Exists) { Logger.Log("Saved Games directory does not exist - attempting to create one."); try { savedGamesFolder.Create(); } catch { } } } if (Updater.CustomComponents != null) { Logger.Log("Removing partial custom component downloads."); foreach (var component in Updater.CustomComponents) { try { SafePath.DeleteFileIfExists(ProgramConstants.GamePath, FormattableString.Invariant($"{component.LocalPath}_u")); } catch { } } } FinalSunSettings.WriteFinalSunIniAsync(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) WriteInstallPathToRegistry(); ClientConfiguration.Instance.RefreshSettings(); using GameClass gameClass = new GameClass(); if (!UserINISettings.Instance.BorderlessWindowedClient) { // Find the largest recommended resolution as the default windowed resolution var bestRecommendedResolution = ScreenResolution.GetBestRecommendedResolution(); UserINISettings.Instance.ClientResolutionX = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, "ClientResolutionX", bestRecommendedResolution.Width); UserINISettings.Instance.ClientResolutionY = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, "ClientResolutionY", bestRecommendedResolution.Height); } else { // Find the largest fullscreen resolution as the default fullscreen resolution var resolution = ScreenResolution.SafeFullScreenResolution; UserINISettings.Instance.ClientResolutionX = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, "ClientResolutionX", resolution.Width); UserINISettings.Instance.ClientResolutionY = new IntSetting(UserINISettings.Instance.SettingsIni, UserINISettings.VIDEO, "ClientResolutionY", resolution.Height); } #if DEBUG // Calculate hashes { FileHashCalculator fhc = new(); fhc.CalculateHashes(); } #endif if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Task.Run(InitSteamworks); gameClass.Run(); } [SupportedOSPlatform("windows")] private void InitSteamworks() { if (UserINISettings.Instance.SteamIntegration) { try { if (ClientConfiguration.Instance.ClientGameType == ClientType.Ares || ClientConfiguration.Instance.ClientGameType == ClientType.YR) { Logger.Log("Steam init called"); SteamClient.Init(2229850); } else if (ClientConfiguration.Instance.ClientGameType == ClientType.TS) { Logger.Log("Steam init called"); SteamClient.Init(2229880); } else if (ClientConfiguration.Instance.ClientGameType == ClientType.RA) { Logger.Log("Steam init called"); SteamClient.Init(2229840); } } catch (System.Exception e) { Logger.Log("Steam init failed: " + e.Message); // Couldn't init for some reason (steam is closed etc) } } } /// /// Recursively deletes all files from the specified directory that were created at or before. /// If directory is empty after deleting files, the directory itself will also be deleted. /// /// Directory to prune files from. /// Time at or before which files must have been created for them to be pruned. private void PruneFiles(DirectoryInfo directory, DateTime pruneThresholdTime) { if (!directory.Exists) return; try { foreach (FileSystemInfo fsEntry in directory.EnumerateFileSystemInfos()) { if ((fsEntry.Attributes & FileAttributes.Directory) == FileAttributes.Directory) PruneFiles(new DirectoryInfo(fsEntry.FullName), pruneThresholdTime); else { try { FileInfo fileInfo = new FileInfo(fsEntry.FullName); if (fileInfo.CreationTime <= pruneThresholdTime) fileInfo.Delete(); } catch (Exception ex) { Logger.Log("PruneFiles: Could not delete file " + fsEntry.Name + ". Error message: " + ex.ToString()); continue; } } } if (!directory.EnumerateFileSystemInfos().Any()) directory.Delete(); } catch (Exception ex) { Logger.Log("PruneFiles: An error occurred while pruning files from " + directory.Name + ". Message: " + ex.ToString()); } } /// /// Move log files from obsolete directories to currently used ones and adjust filenames to match currently used timestamp scheme. /// private void MigrateOldLogFiles() { MigrateLogFiles(SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, "ClientCrashLogs"), "ClientCrashLog*.txt"); MigrateLogFiles(SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, "GameCrashLogs"), "EXCEPT*.txt"); MigrateLogFiles(SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, "SyncErrorLogs"), "SYNC*.txt"); } /// /// Move log files matching given search pattern from specified directory to another one and adjust filename timestamps. /// /// New log files directory. /// Search string the log file names must match against to be copied. Can contain wildcard characters (* and ?) but doesn't support regular expressions. private static void MigrateLogFiles(DirectoryInfo newDirectory, string searchPattern) { DirectoryInfo currentDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, "ErrorLogs"); try { if (!currentDirectory.Exists) return; if (!newDirectory.Exists) newDirectory.Create(); foreach (FileInfo file in currentDirectory.EnumerateFiles(searchPattern)) { string filenameTS = Path.GetFileNameWithoutExtension(file.Name); string[] ts = filenameTS.Split(new string[] { "_" }, StringSplitOptions.RemoveEmptyEntries); string timestamp = string.Empty; string baseFilename = Path.GetFileNameWithoutExtension(ts[0]); if (ts.Length >= 6) { timestamp = string.Format("_{0}_{1}_{2}_{3}_{4}", ts[3], ts[2].PadLeft(2, '0'), ts[1].PadLeft(2, '0'), ts[4].PadLeft(2, '0'), ts[5].PadLeft(2, '0')); } string newFilename = SafePath.CombineFilePath(newDirectory.FullName, baseFilename, timestamp, file.Extension); file.MoveTo(newFilename); } if (!currentDirectory.EnumerateFiles().Any()) currentDirectory.Delete(); } catch (Exception ex) { Logger.Log("MigrateLogFiles: An error occured while moving log files from " + currentDirectory.Name + " to " + newDirectory.Name + ". Message: " + ex.ToString()); } } /// /// Writes processor, graphics card and memory info to the log file. /// [SupportedOSPlatform("windows")] private static void CheckSystemSpecifications() { string cpu = string.Empty; string videoController = string.Empty; string memory = string.Empty; ManagementObjectSearcher searcher; try { searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"); foreach (var proc in searcher.Get()) { cpu = cpu + proc["Name"].ToString().Trim() + " (" + proc["NumberOfCores"] + " cores) "; } } catch { cpu = "CPU info not found"; } try { searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController"); foreach (ManagementObject mo in searcher.Get()) { var currentBitsPerPixel = mo.Properties["CurrentBitsPerPixel"]; var description = mo.Properties["Description"]; if (currentBitsPerPixel != null && description != null) { if (currentBitsPerPixel.Value != null) videoController = videoController + "Video controller: " + description.Value.ToString().Trim() + " "; } } } catch { cpu = "Video controller info not found"; } try { searcher = new ManagementObjectSearcher("Select * From Win32_PhysicalMemory"); ulong total = 0; foreach (ManagementObject ram in searcher.Get()) { total += Convert.ToUInt64(ram.GetPropertyValue("Capacity")); } if (total != 0) memory = "Total physical memory: " + (total >= 1073741824 ? total / 1073741824 + "GB" : total / 1048576 + "MB"); } catch { cpu = "Memory info not found"; } Logger.Log(string.Format("Hardware info: {0} | {1} | {2}", cpu.Trim(), videoController.Trim(), memory)); } /// /// Generate an ID for online play. /// private static void GenerateOnlineId() { #if !WINFORMS if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { #endif #pragma warning disable format try { ManagementObjectCollection mbsList = null; ManagementObjectSearcher mbs = new ManagementObjectSearcher("Select * From Win32_processor"); mbsList = mbs.Get(); string cpuid = ""; foreach (ManagementObject mo in mbsList) cpuid = mo["ProcessorID"].ToString(); ManagementObjectSearcher mos = new ManagementObjectSearcher("SELECT * FROM Win32_BaseBoard"); var moc = mos.Get(); string mbid = ""; foreach (ManagementObject mo in moc) mbid = (string)mo["SerialNumber"]; string sid = new SecurityIdentifier((byte[])new DirectoryEntry(string.Format("WinNT://{0},Computer", Environment.MachineName)).Children.Cast().First().InvokeGet("objectSID"), 0).AccountDomainSid.Value; Connection.SetId(cpuid + mbid + sid); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); key.SetValue("Ident", cpuid + mbid + sid); } catch (Exception) { Random rn = new Random(); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); string str = rn.Next(Int32.MaxValue - 1).ToString(); try { Object o = key.GetValue("Ident"); if (o == null) key.SetValue("Ident", str); else str = o.ToString(); } catch { } Connection.SetId(str); } #pragma warning restore format #if !WINFORMS } else { try { string machineId = File.ReadAllText("/var/lib/dbus/machine-id"); Connection.SetId(machineId); } catch (Exception) { Connection.SetId(new Random().Next(int.MaxValue - 1).ToString()); } } #endif } /// /// Writes the game installation path to the Windows registry. /// [SupportedOSPlatform("windows")] private static void WriteInstallPathToRegistry() { if (!UserINISettings.Instance.WritePathToRegistry) { Logger.Log("Skipping writing installation path to the Windows Registry because of INI setting."); return; } Logger.Log("Writing installation path to the Windows registry."); try { using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); key.SetValue("InstallPath", ProgramConstants.GamePath); } catch { Logger.Log("Failed to write installation path to the Windows registry"); } } } } ================================================ FILE: DXMainClient/app.PerMonitorV2.manifest ================================================  true/PM PerMonitorV2, PerMonitor true ================================================ FILE: DXMainClient/app.SystemAware.manifest ================================================  true true ================================================ FILE: Directory.Build.props ================================================ 14.0 false false disable CnCNet Client CnCNet CnCNet Client Copyright © CnCNet, Rampastring 2011-2026 CnCNet false false UniversalGLDebug;WindowsDXDebug;WindowsGLDebug;WindowsXNADebug; UniversalGLRelease;WindowsDXRelease;WindowsGLRelease;WindowsXNARelease WindowsDX UniversalGL WindowsGL WindowsXNA net48;net8.0-windows net8.0 AnyCPU;x86 x86 AnyCPU x86 AnyCPU true netstandard2.0 AnyCPU net8.0;net48 AnyCPU $(DefineConstants);DEBUG $(DefineConstants);DX $(DefineConstants);GL $(DefineConstants);XNA $(DefineConstants);ISWINDOWS $(DefineConstants);WINFORMS Debug Release $(ClientConfiguration)\$(Engine)\ $(BaseOutputPath)bin\$(OutputPathSuffix) $(BaseIntermediateOutputPath)obj\$(OutputPathSuffix) $(OutputPathSuffix)$(TargetFramework) $(MSBuildProgramFiles32)\dotnet\dotnet.exe $(MSBuildThisFileDirectory)Rampastring.XNAUI $(XNAUIRoot)\References\XNA\Microsoft.Xna.Framework.dll $(XNAUIRoot)\References\XNA\Microsoft.Xna.Framework.Game.dll $(XNAUIRoot)\References\XNA\Microsoft.Xna.Framework.Graphics.dll ================================================ FILE: Directory.Build.targets ================================================ $(DefineConstants);DEVELOPMENT_BUILD <_lib_x64 Include="$(OutputPath)\x64\*.*" /> <_lib_x86 Include="$(OutputPath)\x86\*.*" /> <_GLAndroidDirectories Include="$([System.IO.Directory]::GetDirectories($(PublishDir)runtimes/, 'android*', System.IO.SearchOption.TopDirectoryOnly))" /> <_GLIosDirectories Include="$([System.IO.Directory]::GetDirectories($(PublishDir)runtimes/, 'ios*', System.IO.SearchOption.TopDirectoryOnly))" /> <_CommonAssembliesFilePath Condition="'$(TargetFrameworkIdentifier)' != '.NETFramework'">$(MSBuildThisFileDirectory)CommonAssemblies.txt <_CommonAssembliesFilePath Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">$(MSBuildThisFileDirectory)CommonAssembliesNetFx.txt $(PublishDir)\..\Updater\ ================================================ FILE: Directory.Packages.props ================================================ true 8.0.0 ================================================ FILE: Docs/Build.md ================================================ # Build & Publish # The information below describes the steps that the default build script (build.ps1) performs. Engine configurations --------------------- The client uses different APIs to render itself, called Engine. Engine defines the technology used and the platform where the client can be launched: Any platform (Windows, Linux, macOS): * UniversalGL Only Windows: * WindowsDX * WindowsGL * WindowsXNA TargetFramework configurations ------------------------------ For each Engine configuration one or more TargetFrameworks will be build: UniversalGL: * net8.0 WindowsDX, WindowsGL & WindowsXNA: * net4.8 Overview of the Engine configurations differences: | Configuration | OS Support | Default Platform | Technology | | ------------- | ---------- | ---------------- | ----------------------------- | | UniversalGL | Any | AnyCPU | MonoGame DesktopGL | | WindowsDX | Windows | AnyCPU | MonoGame WindowsDX + WinForms | | WindowsGL | Windows | AnyCPU | MonoGame DesktopGL + WinForms | | WindowsXNA | Windows | x86 | Microsoft XNA + WinForms | Build output ------------ The build output when using the `dotnet publish` command is created in `\Compiled`. Launching the client is done by running e.g.: `dotnet clientogl.dll` Building without IDE -------------------- To build the client without Visual Studio you should install the .NET 8.0 SDK, PowerShell 7.0 and run `Script\Build.bat`. Compiled result will be placed to `Compiled` folder in the root of the repository. Building with Visual Studio --------------------------- > [!IMPORTANT] > IDEs can build Release configurations, but they are forbidden to run due to compile-time optimizations on binaries. You can select the desired configuration directly from the solution configurations: ![VSBUILDCONF](Images/vs-build-configuration.png) Note that the XNA configurations can only be built/debugged with `x86`. ![VSBUILDCPU](Images/vs-cpu-configuration.png) > [!WARNING] > After changing the solution configuration in Visual Studio you *have to* manually execute `dotnet restore` through cmd/powershell in the project's root directory to load packages and also reboot Visual Studio to exclude [NETSDK1004](https://learn.microsoft.com/en-us/dotnet/core/tools/sdk-errors/netsdk1004) and [NETSDK1005](https://learn.microsoft.com/en-us/dotnet/core/tools/sdk-errors/netsdk1005) errors. Building with Rider ------------------- Select desired configuration from list in the top panel: ![RIDERCONF](Images/rider-configuration-dropdown.png) ================================================ FILE: Docs/DiscordRichPresence.md ================================================ # Discord Rich Presence support in XNA CnCNet client About Discord Rich Presence ----------------------------------- Discord Rich Presence (DRP) is a useful feature that allows Discord users to show the details about the currently active game or application to other users. With DRP existing players can show their activity to their friends and spread awareness of your mod/game, thus increasing popularity. ![DEMO](Images/drp-demo.gif) The client shows lobby type, name, map/mission name, gamemode, players amount, available slots, whether the player is ingame, time spent, player faction etc. depending on the current user's activity. XNA client supports showing DRP information customized to your mod/game, provided you follow the steps below to set up the presence for your mod/game. How to set up DRP for your mod/game ----------------------------------- > [!NOTE] > You are required to be logged in a Discord account. 1. Go to [Discord developers portal](https://discord.com/developers/applications). 2. Click the `New Application` button. Type the name of your mod, agree with Discord's policy by clicking on "policy" checkbox and click `Create` button. 3. In `General Information` tab of your application you can find your `Application ID`. You should insert it as a value of `Resource/ClientDefinitions.ini`->`[Settings]`->`DiscordAppId` key. ![ID](Images/drp-id.png) 4. In `Rich Presence` → `Art Assets` tab you need to upload client/mod logo and faction logos via the `Add Image(s)` button. You should upload the images named as follows: - the **game/mod logo** named as `logo` in application assets (adding the app image in Discord *application info* is **not the same** and won't be displayed in user's flyout); - the **icons for factions, random selectors and spectator** should have names consisting of only alphanumerics lowercased (they must pass by [RegExp](https://regexr.com) `[a-z]|[0-9]`). You have to take the *unlocalized* name, lowercase it and remove all non-alphanumerics. For example: - `Nod Genesis Legion` -> `nodgenesislegion`, - `Yuri's Legi0n` -> `yurislegi0n`, - `Random Allies` -> `randomallies`, - `Spectator` -> `spectator` etc. After you upload the images, click the `Save Changes` button. ![ASSETS](Images/drp-assets.png) > [!NOTE] > It may take some time before Discord updates your application info or assets. If you change the assets and app info while running the client - try restarting the Discord and/or client if they don't apply right away. ================================================ FILE: Docs/HowToUpdate.md ================================================ # How to update your mod to latest client version This guide outlines the steps for updating the XNA CnCNet Client version for any mod or game package that is using it (like, for example, Tiberian Sun Client, CnCNet YR, YR Mod Base or any mod that derives from them etc.). > [!WARNING] > It is also strongly recommended to keep the client launcher (the EXE file that resides in the mod folder and launches the actual client) up to date. To update - download the latest release from [it's repository](https://github.com/CnCNet/xna-cncnet-client-launcher/), open the EXE file with [Resource Hacker](https://www.angusj.com/resourcehacker/), change the icons, save and replace the EXE you currently have (for example, `TiberianSun.exe` or `CnCNetYRLauncher.exe`). Don't forget also to copy the rest of the files from the archive to the game folder! ## Updating the XNA CnCNet Client binaries for the package 1. **Download the latest client binaries release:** - Find the latest released client from [XNA CnCNet Client repo releases page](https://github.com/CnCNet/xna-cncnet-client/releases). - Download the `[xna-cncnet-client-X.Y.Z.7z]` file, inside of which the updated `Resources` folder should reside. - Make note of any migration steps noted in the release. 2. **Clean up old binaries folders:** - Go to your local game/mod repo or working folder. - Find `Resources/` folder inside of the "game root" folder. - **IMPORTANT:** Delete `Binaries` and `BinariesNET8` to ensure that no obsolete/renamed library files remain. 3. **Paste files into the package repository:** - Go to your local game/mod repo or working folder. - Unarchive `Resources` folder from `[xna-cncnet-client-X.Y.Z.7z]` file downloaded earlier inside the "game root" folder. - You **must** get a prompt to replace `Resources/` folder and files inside it. If not, you're in the wrong directory. 4. **Apply the migration steps:** - If updating to next version: follow the instructions from release notes mentioned in step 1. - If updating skipping multiple versions, either: - look up all release notes skipped and apply migrations; - or refer to the [client docs on migration](Migration.md). 5. **Run the packaged client to test:** - Launch: `YourClientLauncher.exe` on Windows or `YourClientLauncher.sh` on Linux/Mac (the names will vary depending on the mod/game client package). - Verify the version hash and client version in the online lobby. - Does it work? If no - you missed some migration steps or screwed up somewhere in the steps above, verify your changes or start anew. After that you can commit/push the changes, if using Git, or publish an update ================================================ FILE: Docs/INISystem.md ================================================ # Instructions on how to construct the UI using INI files. > [!NOTE] > _TODO work in progress_ ## Constants The `[ParserConstants]` section of the `GlobalThemeSettings.ini` file contains constants that can be used in other INI files. ### Predefined System Constants `RESOLUTION_WIDTH`: the width of the window when it is initialized `RESOLUTION_HEIGHT`: the height of the window when it is initialized ### User Defined Constants ```ini MY_EXAMPLE_CONSTANT=15 ``` The above user-defined or system constants can be used elsewhere as: ```ini [MyExampleControl] $X=MY_EXAMPLE_CONSTANT ``` _NOTE: Constants can only be used in [dynamic control properties](#dynamic-control-properties)_ ### Data Types - The `text` use `@` as a line break. To write the real `@` character, use `\@`. Also as INI syntax uses `;` to denote comments, use `\semicolon` to write the real `;` character. - The `color` use string form `R,G,B` or `R,G,B,A`. All values must be between `0` and `255`. Example: `255,255,255`, `255,255,255,255`. - The `boolean` string value parses as `true` if it contains one of these symbol as first character: `t`, `y`, `1`, `a`, `e`; and if first symbol is `n`, `f`, `0`, then it parses as `false`. - The `integer` type is actually `System.Int32`. - The `float` type is actually `System.Single`. - The `N integers` or `N floats` is a `integer` or `float` type values repeated `N` times, but separated with `,` character without spaces e.g., `0,0` or `0.0,0.0` for `2 integers` or `2 floats` respectively. - The `comma-separated strings` is a string, but separated with `,` character without spaces e.g., `one,two,three`. ## Control Properties Below lists basic and dynamic control properties. Ordering of properties is important. If there is a property that relies on the size of a control, the properties must set the size of that control first. ### Basic Control Properties Basic control properties cannot use constants. > [!WARNING] > Do not copy-paste ini-code below without edits because it won't work! It shows only how to work with properties. > > For example, > - `X` and `Y` are conflict with `Location`, > - `BackgroundTexture` and `SolidColorBackgroundTexture` conflicts, > - and many others. #### [XNAControl](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAControl.cs) - Basic class inherited by any other control element. ```ini [SOMECONTROL] ; XNAControl X= ; integer, the X location of the control. Y= ; integer, the Y location of the control. Location= ; 2 integers, the X and Y location of the control. Width= ; integer, the Width of the control. Height= ; integer, the Height of the control. Size= ; 2 integers, the Width and Height of the control. Text= ; text, the text to display for the control (ex: buttons, labels, etc...). Visible=true ; boolean, whether or not the control should be visible by default. Enabled=true ; boolean, whether or not the control can be interacted with by default. DistanceFromRightBorder=0 ; integer, the distance of the right edge of this control from ; the right edge of its parent. This control MUST have a parent. DistanceFromBottomBorder=0 ; integer, the distance of the bottom edge of this control from the ; bottom edge of its parent. This control MUST have a parent. FillWidth=0 ; integer, this will set the width of this control to fill ; the parent/window MINUS this value, starting from the its X position. FillHeight=0 ; integer, this will set the height of this control to fill ; the parent/window MINUS this value, starting from the its Y position. DrawOrder=0 ; integer, determine the layering order of the control within ; its parent control's list of child controls. UpdateOrder=0 ; integer, determine the layering order of the control within ; its parent control's list of child controls. RemapColor=255,255,255 ; color, this will set a theme defined color based. ControlDrawMode=UniqueRenderTarget ; enum (UniqueRenderTarget | Normal), ; this will set render option to draw control on its own render ; target (`UniqueRenderTarget`) or to draw control on ; the same render target with its parent (`Normal`). ``` #### [XNAIndicator](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAIndicator.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMEINDICATOR] ; XNAIndicator FontIndex=0 ; integer, the index of font loaded from font list. Default value is `0`. HighlightColor=255,255,255 ; color, the text color when cursor above the `XNAIndicator`. AlphaRate=0.1 ; float, the indicator's transparency changing rate per 100 milliseconds. ; If the indicator is transparent, it'll become non-transparent at this rate. ``` #### [XNAPanel](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAPanel.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMEPANEL] ; XNAPanel BorderColor=196,196,196 ; color, this will set a border color based. AlphaRate=0.01 ; float, the panel's transparency changing rate per 100 milliseconds. ; If the panel is transparent, it'll become non-transparent at this rate. BackgroundTexture= ; string, loads a texture with the specific file name with suffix. ; If the texture isn't found from any asset search path, ; returns a dummy texture. SolidColorBackgroundTexture= ; color, this will set background color stretched texture instead of ; user defined picture. DrawBorders=true ; boolean, enables or disables borders drawing for control. ; Borders enabled by default. Padding= ; 4 integers, css-like panel padding in client window e.g., ; `1,2,3,4` where `1` - left, `2` - top, `3` - right, `4` - bottom. DrawMode=Stretched ; enum (Tiled | Centered | Stretched), ; this will set draw mode for panel. ``` #### [XNAExtraPanel](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAExtraPanel.cs) _(inherits [XNAPanel](#XNAPanel))_ ```ini [SOMEEXTRAPANEL] ; XNAExtraPanel BackgroundTexture= ; string, same as XNAControl's `BackgroundTexture`. ``` #### [XNATextBlock](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATextBlock.cs) _(inherits [XNAPanel](#XNAPanel))_ ```ini [SOMETEXTBLOCK] ; XNATextBlock TextColor=196,196,196 ; color, defines text color for text block. ``` #### [XNAMultiColumnListBox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAMultiColumnListBox.cs) _(inherits [XNAPanel](#XNAPanel))_ ```ini [SOMEMULTICOLUMBLISTBOX] ; XNAMultiColumnListBox FontIndex=0 ; integer, the index of font loaded from font list. DrawSelectionUnderScrollbar=yes ; boolean, enable/disable scroll bar, default value is `true`. ColumnWidthN= ; integer, the default columns width in pixels. `N` is integer column index. ColumnX= ; string:integer, the column definition. `string` is a column header text. ; `integer` is a column width in pixels. `X` is an any text. ListBoxYAttribute:Attrname=Value ; string, allows setting list box attributes. `Attrname` is column attribute. ; `Value` is column attribute value. ``` #### [XNATrackbar](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATrackbar.cs) _(inherits [XNAPanel](#XNAPanel))_ ```ini [SOMETRACKBAR] ; XNATrackbar MinValue=0 ; integer, the minumum value available for XNATrackbar. MaxValue=10 ; integer, the maximum value available for XNATrackbar. Value=0 ; integer, the default value available for XNATrackbar. ClickSound= ; string, loads a sound with the specific file name with suffix as XNATrackbar click sound. ``` #### [XNALabel](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNALabel.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMELABEL] ; XNALabel RemapColor=255,255,255 ; color, same as XNAControl's `RemapColor`. TextColor=196,196,196 ; color, determine color of the text in label. FontIndex=0 ; integer, the index of font loaded from font list. AnchorPoint=0.0,0.0 ; 2 floats, this will set a label's text start drawing point. TextShadowDistance=0.1 ; float, the distance between text and its shadow. TextAnchor= ; enum (NONE | LEFT | RIGHT | HORIZONTAL_CENTER | TOP | BOTTOM | VERTICAL_CENTER), ; this will set a text anchor in label draw box. ``` #### [XNAButton](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNAButton.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMEBUTTON] ; XNAButton TextColorIdle=255,255,255 ; color, the text color when cursor isn't above the button. TextColorHover=255,255,255 ; color, the text color when cursor above the button. HoverSoundEffect= ; string, loads a sound with the specific file name with suffix as button hover sound. ClickSoundEffect= ; string, loads a sound with the specific file name with suffix as button click sound. AdaptiveText=true ; boolean, specifies how the client should change the start text drawing position ; in the button to fill all the free space. Default value is `true`. AlphaRate=0.01 ; float, the button's transparency changing rate per 100 milliseconds. ; If the button is transparent, it'll become non-transparent at this rate. FontIndex=0 ; integer, the index of loaded from font list. IdleTexture= ; string, loads a texture with the specific file name with suffix as button idle texture. HoverTexture= ; string, loads a texture with the specific file name with suffix as button hover texture. TextShadowDistance=0.1 ; float, the distance between text and its shadow. ``` #### [XNAClientButton](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientButton.cs) _(inherits [XNAButton](#XNAButton))_ ```ini [SOMECLIENTBUTTON] ; XNAClientButton MatchTextureSize= ; boolean, the button's width and height will match its texture properties. ToolTip= ; text, the tooltip for button. ``` #### [XNAClientToggleButton](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientToggleButton.cs) _(inherits [XNAButton](#XNAButton))_ ```ini [SOMECLIENTTOGGLEBUTTON] ; XNAClientToggleButton CheckedTexture= ; string, loads a texture with the specific file name with suffix as toggle button checked texture. UncheckedTexture= ; string, loads a texture with the specific file name with suffix as toggle button unchecked texture. ToolTip= ; text, the tooltip for toggle button. ``` #### [XNALinkButton](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNALinkButton.cs) _(inherits [XNAClientButton](#XNAClientButton))_ ```ini [SOMELINKBUTTON] ; XNALinkButton URL= ; string, the URL-link for OS Windows. UnixURL= ; string, the URL-link for Unix-like OS. Arguments= ; string, the arguments separated with space for URL-link. ``` #### [XNACheckbox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNACheckBox.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMECHECKBOX] ; XNACheckbox FontIndex=0 ; integer, the index of font loaded from font list. IdleColor=196,196,196 ; color, the the text color when cursor isn't above the checkbox. HighlightColor=255,255,255 ; color, the text color when cursor above the checkbox. AlphaRate=0.1 ; float, the checkbox's transparency changing rate per 100 milliseconds. ; If the checkbox is transparent, it'll become non-transparent at this rate. AllowChecking=true ; boolean, the allows user to check/uncheck checkbox. Checked=true ; boolean, the default checkbox status. ``` #### [XNAClientCheckbox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientCheckBox.cs) _(inherits [XNACheckBox](#XNACheckbox))_ ```ini [SOMECLIENTCHECKBOX] ; XNAClientCheckbox ToolTip= ; text, the tooltip for checkbox. ``` #### [XNADropDown](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNADropDown.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMEDROPDOWN] ; XNADropDown OpenUp=false ; boolean, defines open/close default status. DropDownTexture= ; string, loads a texture with the specific file name with suffix as ; texture when dropdown closed. DropDownOpenTexture= ; string, loads a texture with the specific file name with suffix as ; texture when dropdown opened. ItemHeight=17 ; integer, the height of each dropdown item in pixels. ClickSoundEffect= ; string, loads a sound with the specific file name with suffix as ; dropdown click sound. FontIndex=0 ; integer, the index of font loaded from font list. BorderColor=196,196,196 ; color, the color for dropdown's border line when it open. FocusColor=64,64,64 ; color, the color for dropdown item when cursore above it. BackColor=0,0,0 ; color, the background color dropdown when it open. DisabledItemColor=169,169,169 ; color, the color for disabled dropdown item. OptionX= ; string, the text option for dropdown. `X` is an any text that helps to ; describe this option e.g., `Option_FirstOption`. ; Option_FirstOption=1 ; Option_SecondOption=two ; Option_ThirdOption=33333 ``` #### [XNAClientDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientDropDown.cs) _(inherits XNADropDown)_ ```ini [SOMECLIENTDROPDOWN] ; XNAClientDropDown ToolTip= ; text, tooltip for dropdown. ``` #### [XNAClientColorDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAClientColorDropDown.cs) _(inherits XNAClientDropDown)_ ```ini [SOMECOLORDROPDOWN] ; XNAClientColorDropDown ItemsDrawMode=TextAndIcon ; enum (Text | Icon | TextAndIcon), ; this will set what combination of texture and text should client use. RandomColorTexture=randomicon.png ; string, the file to load as texture for random color. DisabledItemTexture= ; string, the file to load as texture for disabled items, defaults to texture generated from disabled item color ColorTextureHeight= ; int, color icon height in pixels. ColorTextureWidth= ; int, color icon width in pixels. ``` #### [XNATabControl](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATabControl.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMETABCONTROL] ; XNATabControl RemapColor=255,255,255 ; color, the tab text color. TextColor=255,255,255 ; color, the tab text color. TextColorDisabled=169,169,169 ; color, the color for disabled tab. RemoveTabIndexN=false ; boolean, `N` is `integer` equivalent of tab index. ; RemoveTabIndex0=true ``` #### [XNATextBox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNATextBox.cs) _(inherits [XNAControl](#XNAControl))_ ```ini [SOMETEXTBOX] ; XNATextBox MaximumTextLength=2147483647 ; integer, set maximum input string length. ``` #### [XNASuggestionTextBox](https://github.com/Rampastring/Rampastring.XNAUI/blob/master/XNAControls/XNASuggestionTextBox.cs) _(inherits [XNATextBox](#XNATextBox))_ ```ini [SOMESUGGESTIONTEXTBOX] ; XNASuggestionTextBox Suggestion= ; string, set default background text when no text has typed. ``` ### Basic Control Property Examples ```ini [lblExample] X=100 Y=100 Text=Text Sample ToolTip=Big and beautiful tooltip@that help to undestand lblExample. TextColor=255,255,255 Size=100,100 Visible=yes Enabled=false DistanceFromRightBorder=10 DistanceFromLeftBorder=10 FillWidth=10 FillHeight=10 ``` ### Special Controls & Their Properties Some controls are only available under specific circumstances. #### CoopBriefingBox ```ini ; GameLobbyBase.ini [MapPreviewBox_CoopBriefingBox] FontIndex=0 ``` #### GameLobbyBase Controls Following controls are only available as children of `GameLobbyBase` and derived controls. ##### [GameSessionCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Generic/GameSessionCheckBox.cs) _(inherits [XNAClientCheckBox](#XNAClientCheckBox))_ Game option checkbox for the game lobby. Supports broadcasting game options to the CnCNet lobby and displaying them in the game list and filters. ```ini [SOMEGAMESESSIONCHECKBOX] ; GameSessionCheckBox SpawnIniOption= ; string, spawn INI option to set when checked/unchecked. EnabledSpawnIniValue=True ; string, spawn INI value when checkbox is checked. Defaults to `True`. DisabledSpawnIniValue=False ; string, spawn INI value when checkbox is unchecked. Defaults to `False`. CustomIniPath= ; string, custom INI path for map-specific settings. Reversed=false ; boolean, reverse the checkbox behavior. Checked=false ; boolean, initial checked state. MapScoringMode=Irrelevant ; enum (Irrelevant | DenyWhenChecked | DenyWhenUnchecked), ; controls whether the setting affects map scoring. BroadcastToLobby=false ; boolean, include this checkbox in the GAME broadcast to CnCNet lobby. ShowInGameList=false ; boolean, show icon/text in the game list. ShowInGameListOnRight=false ; boolean, show icon on the right side of the game list. Only applies if ; `ShowInGameList` is `true`. ShowInGameInformationPanel=false ; boolean, show icon/text in the game information panel. ShowInGameInformationPanelAsIconOnly=false ; boolean, show only the icon in the game information panel. Only applies if ; `ShowInGameInformationPanel` is `true`. ShowIconInGameLobby=false ; boolean, show icon in the game lobby control. ShowInFilters=false ; boolean, show this setting in the filters panel for game filtering. EnabledIcon= ; string, texture name for the icon when setting is enabled. DisabledIcon= ; string, texture name for the icon when setting is disabled. SortOrder=0 ; integer, display order for icons in GameInformationPanel and GameListBox. ; Lower values appear first. ``` ##### [CampaignCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignCheckBox.cs) _(inherits [GameSessionCheckBox](#GameSessionCheckBox))_ Use this control type for campaign checkboxes in `CampaignSelector.ini`. Inherits all properties from `GameSessionCheckBox`. Additional properties for this control type are shown below. ```ini [SOMECAMPAIGNCHECKBOX] ; CampaignCheckBox ResetToDefaultOnGameExit=false ; boolean, reset the checkbox to default value when the game exits. ``` ##### [GameLobbyCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyCheckBox.cs) _(inherits [GameSessionCheckBox](#GameSessionCheckBox))_ Use this control type for game lobby checkboxes in `GameLobbyBase.ini`. Inherits all properties from `GameSessionCheckBox`. ##### [GameSessionDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Generic/GameSessionDropDown.cs) _(inherits [XNAClientDropDown](#XNAClientDropDown))_ Game option dropdown for the game lobby. Supports broadcasting game options to the CnCNet lobby and displaying them in the game list and filters. ```ini [SOMEGAMESESSIONDROPDOWN] ; GameSessionDropDown Items= ; comma-separated strings, ; comma-separated list of item values for the dropdown. ItemLabels= ; comma-separated strings, ; optional comma-separated list of display labels for items. SpawnIniOption= ; string, spawn INI option to set based on selected item. DefaultIndex=0 ; integer, default selected item index. DataWriteMode=STRING ; enum (STRING | INDEX | BOOLEAN | MAPCODE), ; determines how the value is written to spawn INI. OptionName= ; string, display name for this option. BroadcastToLobby=false ; boolean, include this dropdown in the GAME broadcast to CnCNet lobby. ShowInGameList=false ; boolean, show icon/text in the game list. ShowInGameListOnRight=false ; boolean, show icon on the right side of the game list. Only applies if ; `ShowInGameList` is `true`. ShowInGameInformationPanel=false ; boolean, show icon/text in the game information panel. ShowInGameInformationPanelAsIconOnly=false ; boolean, show only the icon in the game information panel. Only applies if ; `ShowInGameInformationPanel` is `true`. ShowIconInGameLobby=false ; boolean, show icon in the game lobby control. ShowInFilters=false ; boolean, show this setting in the filters panel for game filtering. Icons= ; comma-separated strings, ; texture names for the icons for each dropdown option. Should match the ; number of items. SortOrder=0 ; integer, display order for icons in GameInformationPanel and GameListBox. ; Lower values appear first. ``` ##### [CampaignDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignDropDown.cs) _(inherits [GameSessionDropDown](#GameSessionDropDown))_ Use this control type for campaign dropdowns in `CampaignSelector.ini`. Inherits all properties from `GameSessionDropDown`. ##### [GameLobbyDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyDropDown.cs) _(inherits [GameSessionDropDown](#GameSessionDropDown))_ Use this control type for game lobby dropdowns in `GameLobbyBase.ini`. Inherits all properties from `GameSessionDropDown`. #### XNAOptionsPanel Controls Following controls are only available as children of `XNAOptionsPanel` and derived controls. These currently use basic control properties only. ##### [SettingCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/SettingCheckBox.cs) _(inherits [XNAClientCheckBox](#XNAClientCheckBox))_ ```ini [SOMESETTINGCHECKBOX] ; SettingCheckBox DefaultValue=false ; boolean, default state of the checkbox. Value of `Checked` will be used ; if it is set and this isn't. Otherwise defaults to `false`. SettingSection=CustomSettings ; string, name of the section in settings INI the setting is saved to. SettingKey= ; string, name of the key in settings INI the setting is saved to. ; Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, ; otherwise `CONTROLNAME_Checked`. WriteSettingValue=true ; boolean, enable to write a specific string value to setting INI key ; instead of the checked state of the checkbox. Defaults to `false`. EnabledSettingValue= ; string, value to write to setting INI key if `WriteSettingValue` ; is set and checkbox is checked. DisabledSettingValue= ; string, value to write to setting INI key if `WriteSettingValue` ; is set and checkbox is not checked. RestartRequired=false ; boolean, whether or not this setting requires restarting the client to apply. ParentCheckBoxName= ; string, name of a `XNAClientCheckBox` control to use as a parent checkbox ; that is required to either be checked or unchecked, depending on value ; of ParentCheckBoxRequiredValue for this checkbox to be enabled. ; Only works if name can be resolved to an existing control belonging ; to same parent as current checkbox. ParentCheckBoxRequiredValue=true ; boolean, state required from the parent checkbox for this one to be enabled. ``` ##### [FileSettingCheckBox](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/FileSettingCheckBox.cs) _(inherits [XNAClientCheckBox](#XNAClientCheckBox))_ ```ini [SOMEFILESETTINGCHECKBOX] ; FileSettingCheckBox DefaultValue=false ; boolean, default state of the checkbox. Value of `Checked` ; will be used if it is set and this isn't. Otherwise defaults to `false`. SettingSection= ; string, name of the section in settings INI the setting is saved to. ; Defaults to `CustomSettings`. SettingKey= ; string, name of the key in settings INI the setting is saved to. ; Defaults to `CONTROLNAME_Value` if `WriteSettingValue` is set, ; otherwise `CONTROLNAME_Checked`. RestartRequired=false ; boolean, whether or not this setting requires restarting the client to apply. ParentCheckBoxName= ; string, name of a `XNAClientCheckBox` control to use as a parent checkbox that ; is required to either be checked or unchecked, depending on value of ; `ParentCheckBoxRequiredValue` for this checkbox to be enabled. ; Only works if name can be resolved to an existing control belonging ; to same parent as current checkbox. ParentCheckBoxRequiredValue=true ; boolean, state required from the parent checkbox for this one to be enabled. CheckAvailability=false ; boolean, if set, whether or not the checkbox can be (un)checked depends on if ; the files to copy are actually present. ResetUnavailableValue=false ; boolean, if set together with `CheckAvailability`, checkbox set to a value that ; is unavailable will be reset back to `DefaultValue`. EnabledFileN= ; comma-separated strings, ; files to copy if checkbox is checked. ; `N` starts from 0 and is incremented by 1 until no value is found. ; Expects 2-3 comma-separated strings in following format: ; source path relative to game root folder, destination path ; relative to game root folder and a file operation option ; (see #appendix-file-operation-options). DisabledFileN= ; comma-separated strings, ; files to copy if checkbox is not checked. ; `N` starts from 0 and is incremented by 1 until no value is found. ; Expects 2-3 comma-separated strings in following format: ; source path relative to game root folder, destination path ; relative to game root folder and a file operation option ; (see #appendix-file-operation-options). ``` ##### [SettingDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/SettingDropDown.cs) _(inherits [XNAClientDropDown](#XNAClientDropDown))_ ```ini [SOMESETTINGDROPDOWN] ; SettingDropDown Items= ; comma-separated strings, ; comma-separated list of strings to include as items to display on the dropdown control. DefaultValue=0 ; integer, default item index of the dropdown. SettingSection= ; string, name of the section in settings INI the setting is saved to. Defaults to `CustomSettings`. SettingKey= ; string, name of the key in settings INI the setting is saved to. Defaults to `CONTROLNAME_Value` ; if `WriteSettingValue` is set, otherwise `CONTROLNAME_SelectedIndex`. WriteItemValue=false ; boolean, enable to write selected item value to the setting INI key instead of the ; checked state of the checkbox. RestartRequired=true ; boolean, whether or not this setting requires restarting the client to apply. ``` ##### [FileSettingDropDown](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DTAConfig/Settings/FileSettingDropDown.cs) _(inherits [XNAClientDropDown](#XNAClientDropDown))_ ```ini [SOMEFILESETTINGDROPDOWN] ; FileSettingDropDown Items= ; comma-separated strings, ; comma-separated list of strings to include as items ; to display on the dropdown control. DefaultValue=0 ; integer, default item index of the dropdown. SettingSection=CustomSettings ; string, name of the section in settings INI the setting is saved to. SettingKey=CONTROLNAME_SelectedIndex ; string, name of the key in settings INI the setting is saved to. RestartRequired=false ; boolean, whether or not this setting requires restarting the client to apply. ResetUnavailableValue=false ; boolean, determines if the client would adjust the setting value automatically ; if the current value becomes unavailable. ItemXFileN= ; comma-separated strings, ; files to copy when dropdown item `X` is selected. ; `N` starts from 0 and is incremented by 1 until no value is found. ; Expects 2-3 comma-separated strings in following format: ; source path relative to game root folder, ; destination path relative to game root folder and a file operation option ; (see #appendix-file-operation-options). ``` ##### Appendix: File Operation Options Valid file operation options available for files defined for `FileSettingCheckBox` and `FileSettingDropDown` are as follows: - `AlwaysOverwrite`: Always overwrites the destination file with source file. - `OverwriteOnMismatch`: Overwrites the destination file with source file only if they are different. - `DontOverwrite`: Never overwrites the destination file with source file if destination file is already present. - `KeepChanges`: Carries over the destination file with any changes manually made to by caching the file if deleted by disabling the option and then re-enabling it. - `AlwaysOverwrite_LinkAsReadOnly`: Try to make a hard link (will look the same as the file but the content of the file will be shared) to the source file (copies the file as a fallback if the linking fails). Recommended to use with any binary source files such as `opengl32.dll`, `d3d9.dll`, `dxgi.dll` and not recommended to use with text files. While link is established, source file and target file has property `Read-only` which protects original file and created link from edits. ### Dynamic Control Properties Dynamic Control Properties CAN use constants. These can ONLY be used in parent controls that inherit the `INItializableWindow` class. ```ini $X=10 ; integer, the X location of the control $Y=20 ; integer, the Y location of the control $Width=50 ; integer, the Width of the control $Height=10 ; integer, the Height of the control $TextAnchor=LEFT ; enum (NONE | LEFT | RIGHT | HORIZONTAL_CENTER | TOP | BOTTOM | VERTICAL_CENTER), ; this will set a text anchor in label draw box. ``` ### Dynamic Control Property Examples ```ini [lblExample] $X=100 $X=MY_X_CONSTANT $Y=100 $Y=MY_Y_CONSTANT $Width=100 $Width=MY_WIDTH_CONSTANT $Height=100 $Height=MY_HEIGHT_CONSTANT ``` ## Window Properties Children of [XNAWindow](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientGUI/XNAWindow.cs) that define their own properties. ### [LoadingScreen](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Generic/LoadingScreen.cs) ```ini ; LoadingScreen.ini [LoadingScreen] RandomBackgroundTextures= ; comma-separated list of strings, ; paths of files to use randomly as BackgroundTexture ``` ## [CampaignSelector](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignSelector.cs) ### [pnlMissionPreview](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignSelector.cs) _(inherits [XNAPanel](#XNAPanel))_ You can now set the preview image for each mission in the campaign selector, known as the mission preview panel. To activate this feature, in `Resources` folder, create a `Mission Previews` folder. Then put an image of your desire inside and rename it as `Default.png`. To adjust panel size and position, modify `pnlMissionPreview` in `CampaignSelector.ini`. Inherits all properties from `XNAPanel`. ```ini [pnlMissionPreview] ; XNAPanel ... ``` To configure which preview image in `Resources/Mission Previews` folder to use for each mission, add the `PreviewImage` property in the mission's section in `Battle.ini` and set its value to the path of the image file relative to the `Resources/Mission Previews` folder. In `Battle.ini`: ```ini [YourMissionSection] PreviewImage= ; string, path to the image file relative to the `Resources/Mission Previews` folder to use as mission preview image. ``` If `PreviewImage` property is not set for a mission, `Resources/Mission Previews/Default.png` will be used as default. ### [tbMissionDescription](https://github.com/CnCNet/xna-cncnet-client/blob/develop/DXMainClient/DXGUI/Campaign/CampaignSelector.cs) _(inherits [XNATextBlock](#XNATextBlock))_ This control shows the mission description in the campaign selector. Note that, when mission preview panel is active, the *default* size of mission description text block size will be automatically changed. To adjust the text block size and position, modify `tbMissionDescription` in `CampaignSelector.ini`. Inherits all properties from `XNATextBlock`. ```ini [tbMissionDescription] ; XNATextBlock ... ``` # Global Config Files ## [ClientDefinition](https://github.com/CnCNet/xna-cncnet-client/blob/develop/ClientCore/ClientConfiguration.cs) > [!NOTE] > _TODO work in progress_ The `ClientDefinitions.ini` file defines the client's global settings, including the game type, recommended resolutions and the executable file used to launch the game. In `ClientDefinitions.ini`: ```ini [Settings] TrustedDomains= ; comma-separated list of strings, ; domain names to match links and prevent the message box from appearing before they open by default browser ; example: cncnet.org,github.com,moddb.com ``` ```ini [Settings] SaveSkirmishGameOptions=false ; boolean, whether or not previously used game options in skirmish are saved across client sessions SaveCampaignGameOptions=false ; boolean, whether or not previously used game options in campaign are saved across client sessions ``` ```ini [Settings] CustomMissionPath=Maps/CustomMissions ; path to the folder containing fan-made maps CustomMissionSupplementFile0Extension=csf ; extension of the first supplement file CustomMissionSupplementFile0CopyAs=stringtable99.csf ; target filename for the first supplement file (required if Extension is present) CustomMissionSupplementFile1Extension=pal ; extension of the second supplement file CustomMissionSupplementFile1CopyAs=custommission.pal ; target filename for the second supplement file (required if Extension is present) CustomMissionSupplementFile2Extension=shp ; extension of the third supplement file CustomMissionSupplementFile2CopyAs=custommission.shp ; target filename for the third supplement file (required if Extension is present) ; supplement files that are supposed to be copied to the game folder when a custom mission is played ; the iteration stops if a number is missing (e.g., if File3Extension is missing, only File0, File1, and File2 are processed) ; both Extension and CopyAs must be provided for each file number; each Extension value must be unique - duplicate extensions are not allowed ``` ```ini [Settings] ReturnToMainMenuOnMissionLaunch=true ; whether or not client returns to main menu when launching a mission ``` ```ini [Settings] CampaignTagSelectorEnabled=false ; turns on the campaign tag selector, showing a window to let users choose which group of missions to play ``` ```ini [Settings] CompatibilityCheckExecutables=CnCNetYRLauncher.exe,gamemd.exe,gamemd-spawn.exe ; comma-separated list of strings, to check for DirectDraw compatibility mode issues ``` ```ini [Settings] ShowGameIconInGameList=true ; boolean, whether to show the game icon in the game listing. Defaults to true. ``` ================================================ FILE: Docs/Migration-INI.md ================================================ # Migrating from older versions - INI configuration Migrating to client version [2.11.0.0][client_2.11] or [2.12.0][client_2.12] from pre-2.11.0.0. This guide uses [YR mod base][mod_base] configuration as an example by migrating from commit [`6ce7db7`](https://github.com/Starkku/cncnet-client-mod-base/commit/6ce7db7fd753df329fb435c3aa5ba90505e5f3a2) to [`34efc04`](https://github.com/Starkku/cncnet-client-mod-base/commit/34efc0454c64e4af28e8177e63f3d9546cbbc6fb). The majority of changes also applies to non-YR client configurations. It is **highly recommended** to make a complete backup of your game/mod before starting. ## Edit `ClientDefinitions.ini` Since v2.12, the client has unified different builds among game types. The game type must be defined in `ClientDefinitions.ini` now. - Add `[Settings]->ClientGameType=YR` (defines client behaviour by game. Allowed options: TS, YR, Ares) The way the client is launched on Unix systems has changed. 1. Add `[Settings]->UnixLauncherExe=YRLauncher.sh` (script file name can be anything) 2. Create `YRLauncher.sh` in game directory: ```sh #!/bin/sh cd "$(dirname "$0")" dotnet Resources/BinariesNET8/UniversalGL/clientogl.dll "$@" ``` 3. **OPTIONAL** Add these entries in `[Settings]` (fill with your required/forbidden mod files): ```ini ; Comma-separated list of files required to run the game / mod that are not included in the installation. RequiredFiles= ; Comma-separated list of files that cannot be present to run the game / mod without problems. ForbiddenFiles= ``` ## Add `GameLobbyBase.ini` Unlike in previous versions, skirmish and multiplayer lobbies share a common, abstract base layout. This file is the base layout of all game lobbies (skirmish, LAN, CnCNet). **Game options have been moved from `GameOptions.ini` to this file**. See example configuration below or in [YR mod base][mod_base]:
Click to show file content ```ini [INISystem] BasedOn=GenericWindow.ini [GameLobbyBase] PlayerOptionLocationX=12 ; def=25 PlayerOptionLocationY=25 ; def=24 PlayerOptionVerticalMargin=9 ; def=12 PlayerOptionHorizontalMargin=5 ; def=3 PlayerOptionCaptionLocationY=6 ; def=6 PlayerNameWidth=127 ; def=136 SideWidth=120 ; def=91 ColorWidth=80 ; def=79 StartWidth=50 ; def=49 TeamWidth=50 ; def=46 ; controls $CC00=btnLaunchGame:GameLaunchButton $CC01=btnLeaveGame:XNAClientButton $CC03=MapPreviewBox:MapPreviewBox $CC04=GameOptionsPanel:XNAPanel $CC05=PlayerOptionsPanel:XNAPanel $CC06=lblMapName:XNALabel $CC07=lblMapAuthor:XNALabel $CC08=lblGameMode:XNALabel $CC09=lblMapSize:XNALabel $CC12=lbMapList:XNAMultiColumnListBox $CC13=ddGameMode:XNAClientDropDown $CC14=lblGameModeSelect:XNALabel $CC15=btnPickRandomMap:XNAClientButton $CC16=tbMapSearch:XNASuggestionTextBox $CC17=PlayerExtraOptionsPanel:PlayerExtraOptionsPanel $CC18=lbChatMessages:ChatListBox $CC19=tbChatInput:XNAChatTextBox $CC20=panelBorderTop:XNAExtraPanel $CC21=panelBorderBottom:XNAExtraPanel $CC22=panelBorderLeft:XNAExtraPanel $CC23=panelBorderRight:XNAExtraPanel $CC24=panelBorderCornerTL:XNAExtraPanel $CC25=panelBorderCornerTR:XNAExtraPanel $CC26=panelBorderCornerBL:XNAExtraPanel $CC27=panelBorderCornerBR:XNAExtraPanel [lblName] Text=Players; in the game its Players, makes more sense than Name actually, eh [lblSide] Text=Side [lblStart] Text=Start Visible=true [lblColor] Text=Color [lblTeam] Text=Team [ddPlayerStartBase] Enabled=true Visible=true [ddPlayerStart0] $BaseSection=ddPlayerStartBase [ddPlayerStart1] $BaseSection=ddPlayerStartBase [ddPlayerStart2] $BaseSection=ddPlayerStartBase [ddPlayerStart3] $BaseSection=ddPlayerStartBase [ddPlayerStart4] $BaseSection=ddPlayerStartBase [ddPlayerStart5] $BaseSection=ddPlayerStartBase [ddPlayerStart6] $BaseSection=ddPlayerStartBase [ddPlayerStart7] $BaseSection=ddPlayerStartBase [lbMapList] $X=LOBBY_EMPTY_SPACE_SIDES $Y=EMPTY_SPACE_TOP + 33 $Width=getWidth($ParentControl) - (getX($Self) + (getWidth(MapPreviewBox) + LOBBY_EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING)) $Height=getBottom(MapPreviewBox) - getY($Self) SolidColorBackgroundTexture=0,0,0,128 [ddGameMode] $Width=180 $Height=DEFAULT_CONTROL_HEIGHT $X=getRight(lbMapList) - getWidth($Self) $Y=getY(lbMapList) - getHeight($Self) - EMPTY_SPACE_TOP [lblGameModeSelect] Text=Game mode: $X=getX(ddGameMode) - getWidth($Self) - LABEL_SPACING $Y=getY(ddGameMode) + 1 [btnMapSortAlphabetically] Visible=false Enabled=false [btnLaunchGame] Text=Launch Game $Width=BUTTON_WIDTH_133 $Height=DEFAULT_BUTTON_HEIGHT $X=LOBBY_EMPTY_SPACE_SIDES $Y=getHeight($ParentControl) - getHeight($Self) - EMPTY_SPACE_BOTTOM [btnPickRandomMap] Text=Pick Random Map $Width=BUTTON_WIDTH_133 $Height=DEFAULT_BUTTON_HEIGHT $X=LOBBY_EMPTY_SPACE_SIDES $Y=getY(btnLaunchGame) - getHeight($Self) - LOBBY_PANEL_SPACING [tbMapSearch] Suggestion=Search map... $Width=getRight(lbMapList) - getRight(btnPickRandomMap) - LOBBY_PANEL_SPACING $Height=DEFAULT_BUTTON_HEIGHT ;DEFAULT_CONTROL_HEIGHT $X=getRight(btnPickRandomMap) + LOBBY_PANEL_SPACING $Y=getY(btnPickRandomMap) ; + 1 BackColor=255,255,255 ;SolidColorBackgroundTexture=0,0,0,128 [MapPreviewBox] $Width=812 $Height=380 $X=getWidth($ParentControl) - getWidth($Self) - LOBBY_EMPTY_SPACE_SIDES $Y=292 SolidColorBackgroundTexture=0,0,0,128 [lblMapName] $Height=DEFAULT_LBL_HEIGHT $X=getX(MapPreviewBox) $Y=getBottom(MapPreviewBox) + LABEL_SPACING [lblMapAuthor] $TextAnchor=LEFT $AnchorPoint=getRight(MapPreviewBox),getY(lblMapName) [lblGameMode] $Height=DEFAULT_LBL_HEIGHT $X=getX(lblMapName) $Y=getBottom(lblMapName) + LABEL_SPACING [lblMapSize] $Height=DEFAULT_LBL_HEIGHT $X=getX(lblGameMode) $Y=getBottom(lblGameMode) + LABEL_SPACING [btnLeaveGame] Text=Leave Game $Width=BUTTON_WIDTH_133 $Height=DEFAULT_BUTTON_HEIGHT $X=getWidth($ParentControl) - getWidth($Self) - LOBBY_EMPTY_SPACE_SIDES $Y=getY(btnLaunchGame) [PlayerOptionsPanel] $X=getX(MapPreviewBox) $Y=EMPTY_SPACE_TOP $Width=getWidth($ParentControl) - (getX($Self) + (getWidth(GameOptionsPanel) + LOBBY_EMPTY_SPACE_SIDES + LOBBY_PANEL_SPACING)) $Height=getHeight(GameOptionsPanel) SolidColorBackgroundTexture=0,0,0,128 $CC00=btnPlayerExtraOptionsOpen:XNAClientButton [PlayerExtraOptionsPanel] $Width=238 $Height=247 $X=getRight(PlayerOptionsPanel) - getWidth($Self) $Y=getY(PlayerOptionsPanel) SolidColorBackgroundTexture=0,0,0,128 [btnPlayerExtraOptionsOpen] $Width=OPEN_BUTTON_WIDTH $Height=OPEN_BUTTON_HEIGHT $X=getWidth($ParentControl) - getWidth($Self) $Y=0 IdleTexture=optionsButton.png HoverTexture=optionsButton_c.png [GameOptionsPanel] $Width=330 $Height=270 $X=getWidth($ParentControl) - getWidth($Self) - LOBBY_EMPTY_SPACE_SIDES $Y=EMPTY_SPACE_TOP SolidColorBackgroundTexture=0,0,0,128 ; Left column checkboxes $CC-GO01=chkShortGame:GameLobbyCheckBox $CC-GO02=chkRedeplMCV:GameLobbyCheckBox $CC-GO03=chkSuperWeapons:GameLobbyCheckBox $CC-GO04=chkCrates:GameLobbyCheckBox $CC-GO05=chkBuildOffAlly:GameLobbyCheckBox $CC-GO06=chkMultiEngineer:GameLobbyCheckBox ; Right column checkboxes $CC-GO07=chkIngameAllying:GameLobbyCheckBox $CC-GO08=chkStolenTech:GameLobbyCheckBox $CC-GO09=chkNavalCombat:GameLobbyCheckBox $CC-GO10=chkDestroyableBridges:GameLobbyCheckBox $CC-GO11=chkBrutalAI:GameLobbyCheckBox $CC-GO12=chkNoSpawnPreview:GameLobbyCheckBox ; Dropdowns $CC-GODD01=cmbCredits:GameLobbyDropDown $CC-GODD02=lblCredits:XNALabel ; $CC-GODD03=cmbGameSpeedCap:GameLobbyDropDown ; different in MP and SP $CC-GODD03PH=cmbGameSpeedCapPlaceholder:XNAControl $CC-GODD04=lblGameSpeedCap:XNALabel $CC-GODD05=cmbTechLevel:GameLobbyDropDown $CC-GODD06=lblTechLevel:XNALabel $CC-GODD07=cmbStartingUnits:GameLobbyDropDown $CC-GODD08=lblStartingUnits:XNALabel $CC01=BtnSaveLoadGameOptions:XNAClientButton [BtnSaveLoadGameOptions] $Width=OPEN_BUTTON_WIDTH $Height=OPEN_BUTTON_HEIGHT $X=getWidth($ParentControl) - getWidth($Self) $Y=0 IdleTexture=optionsButton.png HoverTexture=optionsButton_c.png ;============================ ; LEFT Column Checkboxes ;============================ [chkShortGame] Text=Short Game SpawnIniOption=ShortGame Checked=True ToolTip=Players win when all enemy buildings are destroyed. $X=EMPTY_SPACE_SIDES $Y=EMPTY_SPACE_TOP [chkRedeplMCV] Text=MCV Repacks SpawnIniOption=MCVRedeploy Checked=True ToolTip=Players have the ability to move their command center after it's deployed. $X=getX(chkShortGame) $Y=getBottom(chkShortGame) + GAME_OPTION_ROW_SPACING [chkSuperWeapons] Text=Superweapons SpawnIniOption=Superweapons Checked=False ToolTip=Allow superweapons to be built. $X=getX(chkShortGame) $Y=getBottom(chkRedeplMCV) + GAME_OPTION_ROW_SPACING [chkCrates] Text=Crates Appear SpawnIniOption=Crates Checked=False ToolTip=Random power-up crates will appear. $X=getX(chkShortGame) $Y=getBottom(chkSuperWeapons) + GAME_OPTION_ROW_SPACING [chkBuildOffAlly] Text=Build Off Allies SpawnIniOption=BuildOffAlly Checked=True ToolTip=Allow players to build near their allies' Construction Yards. $X=getX(chkShortGame) $Y=getBottom(chkCrates) + GAME_OPTION_ROW_SPACING [chkMultiEngineer] Text=Multi Engineer SpawnIniOption=MultiEngineer Checked=False ToolTip=Capturing structures requires 3 Engineers instead of 1. $X=getX(chkShortGame) $Y=getBottom(chkBuildOffAlly) + GAME_OPTION_ROW_SPACING ;============================ ; Right Column Checkboxes ;============================ [chkIngameAllying] Text=Ingame Allying SpawnIniOption=AlliesAllowed CustomIniPath=INI/Game Options/AlliesAllowed.ini Checked=True ToolTip=Players can form and break alliances in game. $X=getX(chkShortGame) + GAME_OPTION_COLUMN_SPACING $Y=getY(chkShortGame) [chkStolenTech] Text=Stolen Tech CustomIniPath=INI/Game Options/StolenTech.ini Checked=True ToolTip=Allow infiltration of battle labs for stolen tech infantry. Reversed=yes $X=getX(chkIngameAllying) $Y=getBottom(chkIngameAllying) + GAME_OPTION_ROW_SPACING [chkNavalCombat] Text=Naval Combat CustomIniPath=INI/Game Options/NavalCombat.ini Checked=True ToolTip=Allow shipyards and naval units to be built. Reversed=yes $X=getX(chkIngameAllying) $Y=getBottom(chkStolenTech) + GAME_OPTION_ROW_SPACING [chkDestroyableBridges] Text=Destroyable Bridges CustomIniPath=INI/Game Options/DestroyableBridges.ini Checked=True Location=1038,86 ToolTip=Allow bridges to be destroyed by conventional weapons & force firing. Reversed=yes $X=getX(chkIngameAllying) $Y=getBottom(chkNavalCombat) + GAME_OPTION_ROW_SPACING [chkBrutalAI] Text=Brutal AI CustomIniPath=INI/Game Options/BrutalAI.ini Checked=False Location=1038,107 ToolTip=Makes the AI harder across all levels. $X=getX(chkIngameAllying) $Y=getBottom(chkDestroyableBridges) + GAME_OPTION_ROW_SPACING [chkNoSpawnPreview] Text=No Spawn Previews CustomIniPath=INI/Game Options/NoSpawnPreview.ini Checked=True Location=1038,128 ToolTip=Do not display players' starting locations in loading screen map preview. $X=getX(chkIngameAllying) $Y=getBottom(chkBrutalAI) + GAME_OPTION_ROW_SPACING ;============================ ; Dropdowns ;============================ [lblCredits] Text=Credits: $Height=DEFAULT_LBL_HEIGHT $X=getX(cmbCredits) $Y=getY(cmbCredits) - LABEL_SPACING - DEFAULT_LBL_HEIGHT [cmbCredits] OptionName=Starting Credits Items=50000,45000,40000,35000,30000,25000,20000,15000,10000 DefaultIndex=7 SpawnIniOption=Credits DataWriteMode=String ToolTip=Changes how many credits players start with. $Width=GAME_OPTION_DD_WIDTH $Height=GAME_OPTION_DD_HEIGHT $X=EMPTY_SPACE_SIDES $Y=getHeight($ParentControl) - (getHeight($Self) + EMPTY_SPACE_BOTTOM) [lblGameSpeedCap] Text=Game Speed: $Height=DEFAULT_LBL_HEIGHT $X=getX(cmbGameSpeedCapPlaceholder) $Y=getY(cmbGameSpeedCapPlaceholder) - LABEL_SPACING - DEFAULT_LBL_HEIGHT [cmbGameSpeedCapPlaceholder] Visible=false Enabled=false $Width=GAME_OPTION_DD_WIDTH $Height=GAME_OPTION_DD_HEIGHT $X=getX(cmbCredits) $Y=getY(lblCredits) - LOBBY_PANEL_SPACING - GAME_OPTION_DD_HEIGHT ; not actually a control in this file, used for inheritance [cmbGameSpeedCap] OptionName=Game Speed ; Items= ... DefaultIndex=2 SpawnIniOption=GameSpeed DataWriteMode=Index ToolTip=Changes game speed cap. $Width=getWidth(cmbGameSpeedCapPlaceholder) $Height=getHeight(cmbGameSpeedCapPlaceholder) $X=getX(cmbGameSpeedCapPlaceholder) $Y=getY(cmbGameSpeedCapPlaceholder) [lblTechLevel] Text=Tech Level: $X=getX(cmbTechLevel) $Y=getY(cmbTechLevel) - LABEL_SPACING - DEFAULT_LBL_HEIGHT Enabled=no Visible=no [cmbTechLevel] OptionName=Tech Level Items=10,9,8,7,6,5,4,3,2,1 DefaultIndex=0 SpawnIniOption=TechLevel DataWriteMode=String ToolTip=Changes maximum tech level for all players. $Width=GAME_OPTION_DD_WIDTH $Height=GAME_OPTION_DD_HEIGHT $X=EMPTY_SPACE_SIDES + GAME_OPTION_COLUMN_SPACING $Y=getY(cmbCredits) Enabled=no Visible=no [lblStartingUnits] Text=Starting Units: $X=getX(cmbStartingUnits) $Y=getY(cmbStartingUnits) - LABEL_SPACING - DEFAULT_LBL_HEIGHT [cmbStartingUnits] OptionName=Starting Units Items=10,9,8,7,6,5,4,3,2,1,0 DefaultIndex=10 SpawnIniOption=UnitCount DataWriteMode=String ToolTip=Changes how many infantry units players start with. $Width=GAME_OPTION_DD_WIDTH $Height=GAME_OPTION_DD_HEIGHT $X=getX(cmbTechLevel) $Y=getY(lblTechLevel) - LOBBY_PANEL_SPACING - GAME_OPTION_DD_HEIGHT ; Window Border Sides [panelBorderTop] Location=0,-8 BackgroundTexture=border-top.png DrawMode=Stretched Size=0,9 FillWidth=0 [panelBorderBottom] Location=0,999 BackgroundTexture=border-bottom.png DrawMode=Stretched Size=0,9 FillWidth=0 DistanceFromBottomBorder=-8 [panelBorderLeft] Location=-8,0 BackgroundTexture=border-left.png DrawMode=Stretched Size=9,0 FillHeight=0 [panelBorderRight] Location=999,0 BackgroundTexture=border-right.png DrawMode=Stretched Size=9,0 FillHeight=0 DistanceFromRightBorder=-8 ; Window Border Corners [panelBorderCornerTL] Location=-8,-8 BackgroundTexture=border-corner-tl.png Size=9,9 [panelBorderCornerTR] Location=999,-8 BackgroundTexture=border-corner-tr.png Size=9,9 DistanceFromRightBorder=-8 [panelBorderCornerBL] Location=-8,999 BackgroundTexture=border-corner-bl.png Size=9,9 DistanceFromBottomBorder=-8 [panelBorderCornerBR] Location=999,999 BackgroundTexture=border-corner-br.png Size=9,9 DistanceFromRightBorder=-8 DistanceFromRightBorder=-8 DistanceFromBottomBorder=-8 ```
### Port custom game options If your game/mod has custom game options, you have to port them yourself. To add controls in the game options panel, add `$CC-GO` prefixed list entries in `[GameOptionsPanel]`, then create their own sections. Example option in `GameOptions.ini` in previous versions: ```ini [MultiplayerGameLobby] CheckBoxes=chkNewOption... [SkirmishLobby] CheckBoxes=chkNewOption... [chkNewOption] Text=My New Option CustomIniPath=INI/Game Options/MyNewOption.ini ToolTip=Enable this new option. Checked=False Location=1126,79 ``` Example option in `GameLobbyBase.ini` in the new version: ```ini [GameOptionsPanel] $CC-GONEW=chkNewOption:GameLobbyCheckBox [chkNewOption] Text=My New Option CustomIniPath=INI/Game Options/MyNewOption.ini ToolTip=Enable this new option. Checked=False Location=1126,79 ; $X and $Y are recommended instead ``` ## Edit `SkirmishLobby.ini` This file extends the game lobby base with skirmish-specific controls. **Remove (or port) previous content of this file.** If you have a modified `[SkirmishLobby]` section in `GameOptions.ini`, move it here instead of using the example one below. Remove `CheckBoxes`,`DropDowns` and`Labels` entries; if you have custom game options, see section [Port custom game options](#port-custom-game-options) on how to port them. ```ini [INISystem] BasedOn=GameLobbyBase.ini [SkirmishLobby] $BaseSection=GameLobbyBase [GameOptionsPanel] $CC-GODD03=cmbGameSpeedCapSkirmish:GameLobbyDropDown [cmbGameSpeedCapSkirmish] $BaseSection=cmbGameSpeedCap Items=Fastest (MAX),Faster (60 FPS),Fast (30 FPS),Medium (20 FPS),Slow (15 FPS),Slower (12 FPS),Slowest (10 FPS) ``` ## Edit `MultiplayerGameLobby.ini` This file extends the game lobby base with multiplayer-specific controls, such as the chat box and lock and ready buttons. **Remove (or port) previous content of this file.** If you have a modified `[MultiplayerGameLobby]` section in `GameOptions.ini`, move it here instead of using the example one below. Remove `CheckBoxes`,`DropDowns` and`Labels` entries; if you have custom game options, see section [Port custom game options](#port-custom-game-options) on how to port them.
Click to show file content ```ini [INISystem] BasedOn=GameLobbyBase.ini [MultiplayerGameLobby] $BaseSection=GameLobbyBase PlayerOptionLocationX=36 PlayerOptionLocationY=25 ; def=24 PlayerOptionVerticalMargin=9 ; def=12 PlayerOptionHorizontalMargin=5 ; def=3 PlayerOptionCaptionLocationY=6 ; def=6 PlayerStatusIndicatorX=8 PlayerStatusIndicatorY=0 PlayerNameWidth=135 ; def=136 SideWidth=110 ; def=91 ColorWidth=80 ; def=79 StartWidth=45 ; def=49 TeamWidth=35 ; def=46 ; controls $CCMP00=btnLockGame:XNAClientButton $CCMP01=chkAutoReady:XNAClientCheckBox [lbMapList] $Height=291 [btnPickRandomMap] $Y=getBottom(lbMapList) + LOBBY_PANEL_SPACING [tbMapSearch] $X=getRight(btnPickRandomMap) + LOBBY_PANEL_SPACING $Y=getY(btnPickRandomMap) [lbChatMessagesBase] SolidColorBackgroundTexture=0,0,0,128 $Width=getWidth(lbMapList) $X=LOBBY_EMPTY_SPACE_SIDES [lbChatMessages_Host] $BaseSection=lbChatMessagesBase $Y=getBottom(btnPickRandomMap) + LOBBY_PANEL_SPACING $Height=getBottom(MapPreviewBox) - (getBottom(btnPickRandomMap) + LOBBY_PANEL_SPACING) [lbChatMessages_Player] $BaseSection=lbChatMessagesBase $Y=EMPTY_SPACE_TOP $Height=getBottom(MapPreviewBox) - getY($Self) [tbChatInputBase] Suggestion=Type here to chat... $Width=getWidth(lbMapList) $Height=DEFAULT_CONTROL_HEIGHT $X=LOBBY_EMPTY_SPACE_SIDES $Y=getBottom(MapPreviewBox) + LOBBY_PANEL_SPACING [tbChatInput_Host] $BaseSection=tbChatInputBase [tbChatInput_Player] $BaseSection=tbChatInputBase [btnLockGame] $Width=BUTTON_WIDTH_133 $Height=DEFAULT_BUTTON_HEIGHT $X=getRight(btnLaunchGame) + LOBBY_PANEL_SPACING $Y=getY(btnLaunchGame) [chkAutoReady] Text=Auto-Ready $X=getRight(btnLaunchGame) + LOBBY_PANEL_SPACING $Y=getY(btnLaunchGame) + 2 Enabled=true Visible=true [GameOptionsPanel] $CC-GODD03=cmbGameSpeedCapMultiplayer:GameLobbyDropDown [cmbGameSpeedCapMultiplayer] $BaseSection=cmbGameSpeedCap Items=Fastest (60 FPS),Faster (45 FPS),Fast (30 FPS),Medium (20 FPS),Slow (15 FPS),Slower (12 FPS),Slowest (10 FPS) ```
## Create `CnCNetGameLobby.ini` This file extends the multiplayer game lobby with CnCNet-specific controls, like the change tunnel button. **Remove (or port) previous content of this file.** ```ini [INISystem] BasedOn=MultiplayerGameLobby.ini [MultiplayerGameLobby] $CCMP99=btnChangeTunnel:XNAClientButton [btnChangeTunnel] Text=Change Tunnel $Width=BUTTON_WIDTH_133 $Height=DEFAULT_BUTTON_HEIGHT $X=getX(btnLeaveGame) - getWidth($Self) - LOBBY_PANEL_SPACING $Y=getY(btnLeaveGame) ``` ## Create `LANGameLobby.ini` This stub file can extend the multiplayer lobby with LAN-specifc controls. **Remove (or port) previous content of this file.** ```ini [INISystem] BasedOn=MultiplayerGameLobby.ini ``` ## Edit `GameOptions.ini` After adding all game lobby options to `GameLobbyBase.ini`, remove them here. Remove `[SkirmishLobby]` and `[MultiplayerGameLobby]` sections, too. ## Edit `GenericWindow.ini` Replace `[SkirmishLobby]` and `[MultiplayerGameLobby]` with this: ```ini [GameLobbyBase] BackgroundTexture=gamelobbybg.png DrawBorders=true Size=1230,750 ``` ## Edit `GlobalThemeSettings.ini` This file now also contains the `ParserConstants` section, which lists user-defined constants used for positioning controls within panels and windows. **Without this section, the client will crash with new `GameLobbyBase.ini` layout**. Add the following: ```ini [ParserConstants] DEFAULT_LBL_HEIGHT=12 DEFAULT_CONTROL_HEIGHT=21 DEFAULT_BUTTON_HEIGHT=23 BUTTON_WIDTH_133=133 OPEN_BUTTON_WIDTH=18 OPEN_BUTTON_HEIGHT=22 ;18 EMPTY_SPACE_TOP=12 EMPTY_SPACE_BOTTOM=12 EMPTY_SPACE_SIDES=12 BUTTON_SPACING=12 LABEL_SPACING=6 CHECKBOX_SPACING=24 LOBBY_EMPTY_SPACE_SIDES=12 LOBBY_PANEL_SPACING=10 GAME_OPTION_COLUMN_SPACING=160 GAME_OPTION_ROW_SPACING=6 GAME_OPTION_DD_WIDTH=132 GAME_OPTION_DD_HEIGHT=22 ``` ## Create `ManualUpdateQueryWindow.ini` It is now possible to force a manual query for game/mod updates, which displays a new window. ```ini [INISystem] BasedOn=GenericWindow.ini [btnClose] Location=176,110 ``` ## Edit `OptionsWindow.ini` New checkboxes have been added in the options window. 1. Add sections: ```ini [lblPlayerName] Location=12,195 [tbPlayerName] Location=113,193 [lblNotice] Location=12,220 [btnConfigureHotkeys] Location=12,290 [chkDisablePrivateMessagePopup] Location=12,138 Text=Disable private message pop-ups [chkAllowGameInvitesFromFriendsOnly] Location=276,68 Text=Only receive game invitations@from friends [lblAllPrivateMessagesFrom] Location=276,138 [ddAllowPrivateMessagesFrom] Location=470,137 [gameListPanel] Location=0,200 [btnForceUpdate] ``` 2. **OPTIONAL** Add sections: ```ini [DisplayOptionsPanelExtraControls] 0=chkMEDDraw:FileSettingCheckBox [chkMEDDraw] Location=285,147 Text=Enable DDWrapper for map editor ToolTip=Enables DirectDraw wrapper & emulation for map editor.@Turning this option on can help if you are encountering problems with editor viewport not displaying or being laggy. EnabledFile0=Resources/Compatibility/DLL/ddwrapper.dll,Map Editor/ddraw32.dll,OverwriteOnMismatch EnabledFile1=Resources/Compatibility/Configs/aqrit.cfg,Map Editor/aqrit.cfg,KeepChanges DefaultValue=false SettingSection=Video SettingKey=UseDDWrapperForMapEditor ``` 3. **OPTIONAL (YR+Phobos)** Add sections: ```ini [GameOptionsPanelExtraControls] ; Only available with Phobos 0=chkTooltipsExtra:SettingCheckBox 1=chkPrioritySelection:SettingCheckBox 2=chkBuildingPlacement:SettingCheckBox [chkTooltipsExtra] Location=24,151, ;12,151 Text=Sidebar Tooltip Descriptions ToolTip=Enables additional information in sidebar tooltips. DefaultValue=true ParentCheckBoxName=chkTooltips ParentCheckBoxRequiredValue=true SettingSection=Phobos SettingKey=ToolTipDescriptions [chkPrioritySelection] Location=242,54 Text=Mass Selection Filtering ToolTip=If enabled, non-combat units are not selected if mass-selecting together with combat units. DefaultValue=false SettingSection=Phobos SettingKey=PrioritySelectionFiltering [chkBuildingPlacement] Location=242,78 Text=Show Building Placement Preview ToolTip=If enabled, shows a preview image of the building when placing it. DefaultValue=false SettingSection=Phobos SettingKey=ShowBuildingPlacementPreview ``` ## Create new `PlayerExtraOptionsPanel.ini` A new panel that allows for convenient match setup has been added in the game lobby. ```ini [btnClose] Location=220,0 Size=18,18 [lblHeader] Location=12,6 [chkBoxForceRandomSides] Location=12,28 [chkBoxForceRandomColors] Location=12,50 [chkBoxForceRandomTeams] Location=12,72 [chkBoxForceRandomStarts] Location=12,94 [chkBoxUseTeamStartMappings] Location=12,130 [btnHelp] Location=160,130 [lblPreset] Location=12,156 [ddTeamStartMappingPreset] Size=157,21 Location=65,154 [teamStartMappingsPanel] Location=12,189 ``` ## Appendix For completion's sake, below are additional steps required for a complete migration (beyond INI changes) to client version [2.11.0.0][client_2.11] from pre-2.11.0.0. ### Update client binary files 1. Replace `clientdx.exe`, `clientogl.exe` and `clientxna.exe` in `Resources` with new files. Compiled `.pdb` and `.config` files are optional. 2. Replace contents of `Resources/Binaries` with new files. This directory contains the .NET Framework 4.8 version of the client. 3. **OPTIONAL** Copy contents of downloaded `BinariesNET8` into a new directory `Resources/BinariesNET8`. This directory contains the .NET 8 version of the client that enabled experimental cross-platform Unix support. The `Resources` directory should look like this (omitting configuration files and assets): ```plaintext /Resources # override the `Resources` folder to update the client binaries ├── Binaries # this folder contains partial .NET 4.8 client files ├── BinariesNET8 # this folder contains .NET 8.0 client files, where modders can either delete it, or keep it for an experimental cross-platform support ├── clientdx.exe # .NET 4.8 client main executable ├── clientdx.exe.config # distributed along with `.exe` file. Can be removed but it is better to keep it. ├── clientdx.pdb # .pdb file contains debug symbols. It can be either deleted or retained. ├── clientogl.exe # .NET 4.8 client main executable ├── clientogl.exe.config # same as above ├── clientogl.pdb # same as above ├── clientxna.exe # .NET 4.8 client main executable ├── clientxna.exe.config # same as above └── clientxna.pdb # same as above ``` ### Update the client launcher The client launcher (that resides in the game directory) has been updated. You can replace the old one with the latest version [here](https://github.com/CnCNet/xna-cncnet-client-launcher/releases). Remember to rename it from `CncNetLauncherStub.exe` to your launcher name, i.e. `YRLauncher.exe`, `MentalOmegaLauncher.exe`. Rename the `.config` file appropriately, i.e. `YRLauncher.exe.config`, `MentalOmegaLauncher.exe.config`. ### Keep the old second-stage updater The second-stage updater (formerly `clientupdt.dat`) has been reworked as `SecondStageUpdater.exe`, and will be automatically copied to `Resources/Binaries/Updater` directory by the build script. The old updater will still work, but is no longer maintained. However, don't remove the old updater (`clientupdt.dat`) so that end-users are able to update via the old client. ### Add new assets Every file here can be placed either in `Resources` or in theme directories: - `favActive.png` and `favInactive.png`, 21x21 pixels - `optionsButton.png`, `optionsButton_c.png`, `optionsButtonActive.png`, `optionsButtonActive_c.png`, `optionsButtonClose.png` and `optionsButtonClose_c.png`, 18x18 pixels - `questionMark.png` and `questionMark_c.png`, 18x18 pixels - `sortAlphaAsc.png`, `sortAlphaDesc.png` and `sortAlphaNone.png`, 21x21 pixels - `statusAI.png`, `statusClear.png`, `statusEmpty.png`, `statusError.png`, `statusInProgress.png`, `statusOk.png`, `statusUnavailable.png`, `statusWarning.png`, 21x21 pixels You can find example assets in the [YR mod base][mod_base]. [client_2.11]: https://github.com/CnCNet/xna-cncnet-client/releases/tag/2.11.0.0 [client_2.12]: https://github.com/CnCNet/xna-cncnet-client/releases/tag/2.12.0 [mod_base]: https://github.com/Starkku/cncnet-client-mod-base ================================================ FILE: Docs/Migration.md ================================================ Migrating from older versions ----------------------------- This document lists all the breaking changes and how to address them. Each section corresponds to the migration steps that are required to upgrade to the selected version. If you're skipping multiple versions in the upgrade process - you have to apply all corresponding migration steps. > [!NOTE] > You should always delete the `Binaries` and `BinariesNET8` folders when updating. See [How to update to latest client version](HowToUpdate.md) guide for a step-by-step process of updating the client binaries in your mod/game package. ## 2.13.0 - `PlayerExtraOptionsPanel` control in `GameLobbyBase` has been changed from `XNAWindow` to `XNAPanel`. INI file `PlayerExtraOptionsPanel.ini` is no longer parsed for control attributes, and therefore all contents in this file should be appended to `GameLobbyBase.ini`. In addition, the control `chkBoxForceRandomTeams` has been renamed to `chkBoxForceNoTeams`, so please rename the `[chkBoxForceRandomTeams]` section to `[chkBoxForceNoTeams]`. ## 2.12.12 - The `DTAConfig` library has been removed and its functionality merged into other parts of the client. Therefore, if using automatic updater, you must append the following lines to the `[Delete]` section of your `updateexec` file to prevent issues during the update process: In `updateexec`: ```ini [Delete] ; append those lines in the section Resources\Binaries\Windows\DTAConfig.dll Resources\Binaries\Windows\DTAConfig.pdb Resources\Binaries\OpenGL\DTAConfig.dll Resources\Binaries\OpenGL\DTAConfig.pdb Resources\Binaries\XNA\DTAConfig.dll Resources\Binaries\XNA\DTAConfig.pdb Resources\BinariesNET8\Windows\DTAConfig.dll Resources\BinariesNET8\Windows\DTAConfig.pdb Resources\BinariesNET8\OpenGL\DTAConfig.dll Resources\BinariesNET8\OpenGL\DTAConfig.pdb Resources\BinariesNET8\UniversalGL\DTAConfig.dll Resources\BinariesNET8\UniversalGL\DTAConfig.pdb Resources\BinariesNET8\XNA\DTAConfig.dll Resources\BinariesNET8\XNA\DTAConfig.pdb ``` ## 2.12.10 - The `FontIndex` property of `CoopBriefingBox` has been changed from 3 to 0, eliminating all hard-coded font usages except for fonts 0 and 1. Normally, you can ignore this change, but if you do want to use a different font for the map briefing, check the documentation of `CoopBriefingBox` in [INISystem.md](INISystem.md) file. ## 2.12.6 - The color dropdown now defaults to show both text and color. To revert to the text-only behavior, set `ItemsDrawMode=Text` in `[ddPlayerColor0]` to `[ddPlayerColor7]` sections in `GameLobbyBase.ini` file. - It is advised to remove the `Size` property for `[GameCreationWindow]` and `[GameCreationWindow_Advanced]` (might be defined in either `GenericWindow.ini` or `GameCreationWindow.ini`) after upgrading to this version. ## 2.12.0 - The client now has unified different builds among game types. The game type must be defined in the `ClientDefinitions.ini` file. Please specify `ClientGameType` in `[Settings]` section of the `ClientDefinitions.ini` file, e.g., `ClientGameType=Ares`. See [this file](https://github.com/CnCNet/xna-cncnet-client/blob/0554d7974cb741170c881116568144265e6cbabb/ClientCore/Enums/ClientType.cs) for a list of available values. - The `trbScrollRate` component in `GameOptionsPanel` was mistakenly named as `trbClientVolume` in the INI configuration file from the beginning. This has been fixed. No action is needed unless this component was explicitly modified in your configuration (rare). ## 2.11.0.0 and earlier - `CustomSettingFileCheckBox` and `CustomSettingFileDropDown` have been renamed to simply `FileSettingCheckBox` and `FileSettingDropDown`. This requires adjusting the control names in `OptionsWindow.ini`. `FileSettingCheckBox` has a fallback to legacy behaviour if the control has any files defined with `FileX`. - Updater no longer has hardcoded list of download mirrors or custom components. This information must now be set in `UpdaterConfig.ini` (example is included amongst default resources in client repository). For a reference, the previously hardcoded information can be found in format used by `UpdaterConfig.ini` [here](https://gist.github.com/Starkku/1d52f0040d7a00d79e57afc2fba5f97b). - Second-stage updater no longer has hardcoded list of launcher executables to check for when restarting the client. It will now only check `ClientDefinitions.ini` for `LauncherExe` key, and it it fails to read and launch this the client will not automatically restart after updating. - Updater DLL filename has been changed from `DTAUpdater.dll` to `ClientUpdater.dll` and second-stage updater from `clientupdt.dat` to `SecondStageUpdater.exe` for .NET 4.8 and has been moved from base folder to `Resources/Binaries/Updater`. - **Note:** If you want end-users to be able to update via the old client, it is necessary to preserve a copy of the old second-stage updater (`clientupdt.dat`) in the client base directory. In other words, *don't* modify or delete `clientupdt.dat` with either of the [update server scripts](/Docs/Updater.md). - Second-stage updater is now automatically copied to `Resources/Binaries/Updater` folder by build scripts. - To support launching the game on Linux the file defined as `UnixGameExecutableName` (defaults to `wine-dta.sh`) in `ClientDefinitions.ini` must be set up correctly. E.g. for launching a game with wine the file could contain `wine gamemd-spawn.exe $*` where `gamemd-spawn.exe` is replaced with the game executable. Note that users might need to execute `chmod +x wine-dta.sh` once to allow it to be launched. - The use of `*.cur` mouse cursor files is not supported on the cross-platform `UniversalGL` build. To ensure the intended cursor is shown instead of a missing texture (pink square) all themes need to contain a `cursor.png` file. Existing `*.cur` files will still be used by the Windows-only builds. - The MonoGame MCGB editor will convert the MainMenuTheme to `MainMenuTheme.wma` when publishing for MonoGame WindowsDX. MonoGame DesktopGL only supports the `*.ogg` format. To ensure the MainMenuTheme is available on both the WindowsDX & DesktopGL client versions you need to manually convert and add the missing ogg format file to each theme. Each theme should then contain both `MainMenuTheme.wma` and `MainMenuTheme.ogg` files. The client will then switch out the correct MainMenuTheme format at runtime. - Updated XNAUI [fixes a bug](https://github.com/Rampastring/Rampastring.XNAUI/commit/6857704734241895f9cbb2c79fbd0286c350c313) that causes the border might not be drawn. However, your mod might depends on this bug and therefore the unwanted border appears in window after upgrading. In this case, please manually specify `DrawBorders=false` for your window. For example, add the following lines to `GenericWindow.ini` to turn off borders in *some* windows like the message box. But you still need to specify this property for more windows in the ini file depending on your need. ```ini [GenericWindow] DrawBorders=false ``` - The [Tiberian Sun Client v6 Changes](https://github.com/CnCNet/xna-cncnet-client/pull/275) breaks compatibility. You need to reimplement the ini files for `SkirmishLobby`, `LANLobby`, and `CnCNetLobby` with the new `INItializableWindow` format. Also, add the `[$ExtraControls]` section in `GenericWindow.ini` file if you rely on `[ExtraControls]`. Define constants in `[ParserConstants]` section in `DTACnCNetClient.ini` file, which might be used from the `INItializableWindow` configuration. See [this guide](/Docs/Migration-INI.md) for details. - The new [player status indicators feature](https://github.com/CnCNet/xna-cncnet-client/pull/251) replaces the old "player is ready" indicators in game lobby. This requires: - renaming `PlayerReadyBox*` tags into `PlayerStatusIndicator*` (which now have default values of `0` and `0` instead of `7` and `4` for `X` and `Y` respectively); - providing the following new textures (in `Resources` folder and/or theme subfolders, like in [this example](https://github.com/CnCNet/cncnet-yr-client-package/pull/61)): - `statusEmpty.png`; - `statusUnavailable.png`; - `statusAI.png`; - `statusClear.png`; - `statusOk.png`; - `statusInProgress.png`; - `statusWarning.png`; - `statusError.png`; - The [Tiberian Sun Client v6 Changes](https://github.com/CnCNet/xna-cncnet-client/pull/275) changes the license to GPLv3. This means that if your client is a private fork, you must either stop releasing the modified client or provide the modified source code to public with GPLv3 license. - `BtnSaveLoadGameOptions` in game lobbies was renamed to `btnSaveLoadGameOptions` for consistency. See [this change](https://github.com/CnCNet/cncnet-ts-client-package/commit/2ac97c68978431e94e320299e0168119f75a849f) to TSC for an example of addressing this. ================================================ FILE: Docs/NewFeatures.md ================================================ # New Features This document describes optional, non-breaking changes. While not mandatory, adopting these updates unlocks new client features. Breaking changes are not covered here; see [Migration.md](Migration.md) instead. ## 2.13.0 - Custom mission support and game mode updates offer several new features. Details will be provided later. - The following controls are now available to support broadcasting customized game options to the CnCNet lobby and displaying them in the game list and filters. `GameSessionCheckBox`, `GameLobbyCheckBox`, `GameSessionDropDown`, `GameLobbyDropDown`. See [INISystem.md](INISystem.md). - The game icon in the game lobby list can be turned off. See `ShowGameIconInGameList` in [INISystem.md](INISystem.md). ## 2.12.18 - The `MainMenuTheme` key in the `[General]` section of `DTACnCNetClient.ini` (which may depend on the `GlobalThemeSettings.ini` file) now supports multiple background music files, separated by commas. The client will randomly select one. ## 2.12.17 - This version includes a DirectDraw compatibility fixer that helps users remove problematic compatibility settings from game executable files. It is therefore recommended to add game executable files to the `ClientDefinitions.ini` file. Example: ```ini [Settings] CompatibilityCheckExecutables=CnCNetYRLauncher.exe,gamemd.exe,gamemd-spawn.exe ; comma-separated list of strings to check for DirectDraw compatibility mode issues ``` - A lobby settings update window has been added, allowing the host to change the room name, maximum player count, skill level, and password. To enable this feature, edit the `CnCNetGameLobby.ini` file. First, add `$CCMP100=btnGameLobbySettings:XNAClientButton` (the number may vary depending on your configuration) to the existing `[MultiplayerGameLobby]` section: ```ini [MultiplayerGameLobby] $CCMP100=btnGameLobbySettings:XNAClientButton ``` Then, add the following `[btnGameLobbySettings]` section: ```ini [btnGameLobbySettings] Text=Lobby Settings Location=0,0 Size=133,23 DistanceFromBottomBorder=13 DistanceFromRightBorder=300 Visible=false Enabled=false ``` ## 2.12.15 - The client now supports long-path awareness to handle map files with paths longer than 260 characters, which can occur when downloading custom maps. However, long-path awareness must **also** be enabled on the **player’s machine**. If you use Inno Setup to distribute your mod, you can include the following in the Inno Setup script: ```iss [Registry] Root: HKLM; Subkey: SYSTEM\CurrentControlSet\Control\FileSystem; ValueType: dword; ValueName: LongPathsEnabled; ValueData: 1; MinVersion: 10.0.14393 ``` Alternatively, you can instruct players to enable it by providing the following `.reg` file: ```reg Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem] "LongPathsEnabled"=dword:00000001 ``` ## 2.12.13 - SpriteFont files have been revised. Please download the new [SpriteFont0.xnb](/DXMainClient/Resources/DTA/SpriteFont0.xnb) and [SpriteFont1.xnb](/DXMainClient/Resources/DTA/SpriteFont1.xnb) files and replace the old ones in the `Resources` folder. The client no longer relies on the remaining font files (SpriteFont2, 3, 4, 5, …) unless they are explicitly specified as `FontIndex` in your `.ini` files; you may remove them if unused. ## 2.12.12 - `CampaignSelector` now supports game options and forced spawn options using `CampaignCheckBox` and `CampaignDropDown` components. The keys `SaveSkirmishGameOptions` and `SaveCampaignGameOptions` are also available in the `[Settings]` section of `ClientDefinitions.ini`. ## 2.12.10 - `VersionWriter.exe` has been updated with new settings: `ExcludeHiddenAndSystemFiles`, `ApplyTimestampOnVersion`, `NoCopyMode`, and the `[ExcludeDirectories]` section. See [Updater.md](Updater.md). ## 2.12.8 - You can create a `UserDefaults.ini` file in the `Resources` folder to override default settings in the Options window. Example: ```ini [Video] IntegerScaledClient=True BorderlessWindowedClient=False [Audio] ClientVolume=0.3 PlayMainMenuMusic=False [MultiPlayer] NotifyOnUserListChange=False ``` We recommend specifying `IntegerScaledClient=True` as the default. - For `XNAClientColorDropDown` components, the `DisabledItemTexture` key can be used. ## 2.12.7 - Random selectors defined in the `[RandomSelectors]` section of the `GameOptions.ini` file now accept duplicate values, allowing adjustment of the random weight for each side. ## 2.12.6 - A `MapEncoding` key can be specified in the `Translation.ini` file. However, **you should not specify it** unless you fully understand what you are doing. For example, you should **NOT** select GB2312/GBK/GB18030/BIG5 for a Chinese translation. This feature is primarily intended for Tiberian Sun and should never be used for Red Alert 2. - Three drawing modes are now available for `XNAClientColorDropDown` components. See `XNAClientColorDropDown` in [INISystem.md](INISystem.md). ## 2.12.5 - An inactive host detection feature has been added. To enable it, specify `InactiveHostWarningMessageSeconds` and `InactiveHostKickSeconds` with positive integer values in the `[Settings]` section of `ClientDefinitions.ini`. ## 2.12.4 - The client now displays a warning before opening unknown HTTP/HTTPS links from chat messages. You can override the default list of trusted domains using the `TrustedDomains` key in the `[Settings]` section of `ClientDefinitions.ini`. See [INISystem.md](INISystem.md). ## 2.12.2 - The client now supports randomly selecting one loading screen from multiple images. See the `LoadingScreen` section in [INISystem.md](INISystem.md). ## 2.11.7.0 - Previously, side selection could only be restricted via co-op map settings, game mode settings, or game option checkboxes. This version allows disabling specific sides for human players or AI players separately by using `DisallowedHumanPlayerSides` and `DisallowedComputerPlayerSides` in game mode sections. Example in `INI\MPMaps.ini`: ```ini [Standard] ; any game mode section ; (...) DisallowedPlayerSides=7 ; already exists - disallows sides for all players DisallowedHumanPlayerSides=1,2,3 ; new - disallows sides for human players only DisallowedComputerPlayerSides=4,5,6 ; new - disallows sides for computer players only ``` - The default CnCNet service URLs have been upgraded to HTTPS. If you are using non-HTTPS URLs in `ClientDefinitions.ini` or `NetworkDefinitions.ini`, especially for domains ending in `cncnet.org` or `moddb.com`, please update them to HTTPS. ## 2.11.2.0 - In versions 2.11.0.0 and 2.11.1.0, `ClientUpdater.xml` and `SecondStageUpdater.xml` files were released with the client binaries. These files are not necessary and can be safely removed. ## 2.11.1.0 - The client now offers several integer-scaled resolutions from the recommended list when not in fullscreen mode. Modders are encouraged to update the `RecommendedResolutions` setting in `ClientDefinitions.ini` so that listed resolutions are no smaller than `{MinimumRenderWidth}x{MinimumRenderHeight}` and no larger than `{MaximumRenderWidth}x{MaximumRenderHeight}`. - Documentation has been updated to encourage modders to retain `*.pdb` files corresponding to `*.exe` and `*.dll` files even when distributing to end users (e.g., `clientdx.pdb`, `ClientCore.pdb`, etc.). Keeping these files provides significantly more detailed error logs and greatly aids troubleshooting. - Documentation has been updated to recommend that Chinese translators use `zh-Hans` or `zh-Hant` as the name of the translation folder. ## 2.11.0.0 - A localization system has been implemented. See [Translation.md](Translation.md). - The OpenGL variant of the client can now load background music from an `.ogg` file that is placed alongside the corresponding `.wma` file. - Several network-related definitions can now be customized via `NetworkDefinitions.ini` file in the `Resources` folder. An example is shown below. ```ini [Settings] CnCNetTunnelListURL=https://cncnet.org/master-list CnCNetPlayerCountURL=https://api.cncnet.org/status CnCNetMapDBDownloadURL=https://mapdb.cncnet.org CnCNetMapDBUploadURL=https://mapdb.cncnet.org/upload DisableDiscordIntegration=False ; https://gamesurge.net/servers [IRCServers] 1=irc.gamesurge.net|GameSurge|6667 2=LAN-Team.DE.EU.GameSurge.net|GameSurge Nuremberg, Germany|6660,6666,6667,6668,6669 3=Stockholm.SE.EU.GameSurge.net|GameSurge Stockholm, Sweden|6666,6669,7000,8080 4=NuclearFallout.WA.US.GameSurge.net|GameSurge Seattle, WA|6667,5960 5=Prothid.NY.US.GameSurge.Net|GameSurge NYC, NY|5960,6660,6666,6667,6668,6669,6697 6=192.223.27.109|GameSurge IP 192.223.27.109|5960,6660,6666,6667,6668,6669 7=162.248.94.123|GameSurge IP 162.248.94.123|6667,5960 8=128.140.107.226|GameSurge IP 128.140.107.226|6660,6666,6667,6668,6669 9=188.240.145.60|GameSurge IP 188.240.145.60|6660,6666,6667,6668,6669 ``` ================================================ FILE: Docs/Translation.md ================================================ # Translation The client has a built-in support for translations. The translation system is made to allow non-programmers to easily translate mods and games based on XNA CnCNet client to the languages of their choice. The translation system supports the following: - translating client's built-in text strings; - translating INI-defined text values without modifying the respective INI files themselves; - adjusting INI-defined size and position values for client controls per translation; - providing custom client asset overrides (including both generic and theme-specific) in translations (for instance, translated buttons with text on them, or fonts for different CJK variatons); - auto-detecting the initial language of the client based on the system's language settings (if provided; happens on first start of the client); - configurable set of files to copy to the game directory (for ingame translations); - an ability to generate a translation template/stub file for easy translation. ## Translation structure The translation system reads folders from the `Resources/Translations` directory by default. Each folder found in that directory is considered a translation and can contain the main translation INI (contains some translation metadata and the translated values), generic assets (they take priority over what's found in `Resources` folder under the same relative path), theme-specific translation INIs and theme-specific assets (overrides for `Resources/[theme name]`) placed in the folders with the same names as the main theme folders that they are supposed to override. For example: ```md - Resources - Some Theme Folder * someThemeAsset.png * ... - Translations - ru - Some Theme Folder * Translation.ini * someThemeAsset.png * ... * Translation.ini * someAsset.png * ... - uk * ... - zh-Hans * ... - zh-Hant * ... * someAsset.png * ... ``` ### Folder naming and automatic language detection The translation folder name is used to match it to the system locale code (as defined by BCP-47), so it is advised to name the translation folders according to that (for example, see how [the locales Windows uses](https://learn.microsoft.com/ru-ru/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c) are coded). That allows the client to choose the appropriate translation based on the system locale and also automatically fetch the name of the translation. > [!NOTE] > Unless you're aiming for making a translation for a specific country (e.g. `en-US` and `en-GB`), it's advised to use simply a [language code](http://www.loc.gov/standards/iso639-2/php/code_list.php) (for example, `ru`, `de`, `en`, `zh-Hans`, `zh-Hant` etc.) The folder name doesn't explicitly need to match the existing locale code. However, in that case you would want to provide an explicit name in the translation INI, and the translation won't be automatically picked in any case. > [!NOTE] > The hardcoded client strings can be overridden using an `en` translation. Because the built-in `en` strings are always available, so it English client language. Even if the client doesn't have any translations, English will still be picked by default. If for some reason you need to override hardcoded strings in your client distribution, you can create a `Resources/Translations/en/Translation.ini` file and override needed values there. ### Translation INI format ```ini [General] ; translation metadata Name=Some Language ; string, used instead of a system-provided name if set Author=Someone ; string MapEncoding=UTF-8 ; string, defines the name of the map encoding to be used to load the map files to the spawnmap.ini file. The 'Auto' option means that the client will try to guess the encoding. Please either omit this line or specify 'UTF-8'. Only specify 'Auto' or an encoding different from 'UTF-8' if you really know what you are doing. [Values] ; the key-values for translation Some:Key=Some Value ; string, see below for explanation ``` #### Translation values key format Examples: ```ini INI:HotkeyCategories:Interface=Интерфейс ; Interface INI:Hotkeys:AllToCheer:Description=Приказать вашей пехоте ликовать. ; Make all of your infantry units cheer. INI:Hotkeys:AllToCheer:UIName=Ликовать ; Cheer INI:Controls:CheaterScreen:lblCheater:Text=Обнаружены изменения! ; Modifications Detected! Client:DTAConfig:ForceUpdate=Принудительное обновление ; Force Update INI:Controls:UpdaterOptionsPanel:btnForceUpdate:Location=320,213 INI:Controls:UpdaterOptionsPanel:btnForceUpdate:Size=220,23 ``` Each key in the `[Values]` section is composed of a few elements, joined using `:`, that have different semantic meaning. The structure can be described like this (with list level denoting the position). - `Client` - the client's built-in text strings. - The 2nd and 3rd parts usually denote the string's "namespace" or category and the string's name, respectively, and are chosen arbitrarily by the developers. - `INI` - the INI-defined values. - `Controls` - denotes all INI-defined control values. - `[parent control name]` - the name of the parent control of the control that the value is defined for. Specifying `Global` instead of the parent name allows to specify identical translated value for all instances of the control regardless of the parent (parent-specific definition overrides this still though) - `[control name]` - the name of the control that the value is defined for. - `[attribute name]` - the name of the attribute that is being translated. Currently supported: - `Text`, `Size`, `Width`, `Height`, `Location`, `X`, `Y`, `DistanceFromRightBorder`, `DistanceFromBottomBorder` for every control; - `ToolTip` for controls with tooltip; - `Suggestion` for suggestion text boxes; - `URL`, `UnixURL` for link buttons; - `ItemX` (where X) for setting/game options dropdowns; - `OptionName` for game option dropdowns; - `$X`, `$Y`, `$Width`, `$Height` for INItializable window system. - `Sides` - subcategory for the game's/mod's side names. - `Colors` - subcategory for the game's/mod's color names. - `Themes` - subcategory for the game's/mod's theme names. - `GameModes` - subcategory for the game's/mod's game modes. - `[name]` - uniquely identifies the game mode. - `[attribute name]` - the name of the attribute that is being translated. Only `UIName` is supported. - `Maps` - subcategory for the game's/mod's maps (custom maps are not supported). - `[map path]` - uniquely identifies the map. - `[attribute name]` - the name of the attribute that is being translated. Only `Description` (map name) and `Briefing` are supported. - `Missions` - subcategory for the game's/mod's singleplayer missions. - `[mission section name]` - uniquely identifies the map (taken from `Battle*.ini`). - `[attribute name]` - the name of the attribute that is being translated. Only `Description` (mission name) and `LongDescription` (actual description) are supported. - `CustomComponents` - subcategory for the game's/mod's custom components. - `[custom component INI name]` - uniquely identifies the custom component. - `[attribute name]` - the name of the attribute that is being translated. Only `UIName` is supported. - `UpdateMirrors` - subcategory for the game's/mod's update download mirrors. - `[mirror name]` - uniquely identifies the mirror. - `[attribute name]` - the name of the attribute that is being translated. Only `Name` and `Location` are supported. - `Hotkeys` - subcategory for the game's/mod's hotkeys. - `[INI name]` - uniquely identifies the hotkey. - `[attribute name]` - the name of the attribute that is being translated. Only `UIName` and `Description` are supported. - `HotkeyCategories` - subcategory for the game's/mod's hotkey categories. - `ClientDefinitions` - self explanatory. - `WindowTitle` - self explanatory, only works if set in `ClientDefinitions.ini` > [!WARNING] > You can only translate an INI value if it was used in the INI in the first place! That means that defining a translated value for a control's attribute (example: translating `X` and `Y` when `Location` is defined) that is not present in the INI **will not have any effect**. > [!IMPORTANT] > If the button has an `IdleTexture` key, be sure to place this key as the first key in the button's section, otherwise you will not be able to resize it from `Translation.ini`, because `IdleTexture` changes the size of the button. ## Ingame translation setup The translation system's ingame translation support requires the mod/game author(s) to specify the files which translators can provide in order to translate the game. The files are specified in the the syntax is `GameFileX=path/to/source.file,path/to/destination.file[,checked]` INI key in the `[Translations]` section of `ClientDefinitions.ini` (X is any text you want to add to the key to help sort files), with comma-separated parts of the value meaning the following: 1) the path to the source file relative to currently selected translation directory; 2) the destination to copy to, relative to the game root folder; 3) (optional) `checked` for the file to be checked by file integrity checks (should be on if this file can be used to cheat), if not specified - this file is not checked. > [!IMPORTANT] > When processing the translation game files, by default, the translation system will attempt to create destination files as [hard links](https://learn.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions). If creating a hard link is unsuccessful, the system will instead make copies of the files. > > Translators are advised to always work on files located in the source folder and avoid editing the copies in the destination folder. This is important because when a language is deselected, the client will automatically delete the files in the destination folder. Be aware that even if a source file and the corresponding destination file are hard-linked, editing either file in a text editor might cause one of these two consequences: either both files will be concurrently updated, or the hard link might be broken, causing only the file being edited to receive the updates. This is why it is recommended to always work on the source files. > > To see links in Windows Explorer, you can install [this extension](https://schinagl.priv.at/nt/hardlinkshellext/linkshellextension.html). > [!WARNING] > If you include checked files in your ingame translation files, that means users won't be able to do custom translations if they include those files and you won't be able to use custom components with those files **without triggering the modified files / cheater warning**. This mechanism is made for those games and mods where it's impossible to provide a mechanism to provide translations in a cheat-safe way, so please use it only if you have no other choice, otherwise don't specify this parameter. Example configuration in `ClientDefinitions.ini`: ```ini [Translations] GameFileTranslationMix=translation.mix,expandmo98.mix GameFile_GDI01=Missions/g0.map,Maps/Missions/g0.map GameFile_NOD01=Missions/n0.map,Maps/Missions/n0.map GameFile_DLL_SD=Resources/language_800x600.dll, Resources/language_800x600.dll GameFile_DLL_HD=Resources/language_1024x720.dll,Resources/language_1024x720.dll ``` This will make the `translation.mix` file from current translation folder (say, `Resources/Translations/ru`) copied to game root as `expandmo98.mix` on game start. > [!WARNING] > This feature is needed only for *game* files, not *client* files like INIs, theme assets etc.! ## Suggested translation workflow 0. In the mod's settings INI file (for example: `SUN.INI`, `RA2MD.INI`) append `GenerateTranslationStub=true` in `[Options]` section. This will make the client generate a `Translation.ini` file in `Client` folder with all (almost; read caveat below) translatable text values, sorted alphabetically by key. Values with no translations will be commented out; if some translation was already loaded - then the present values and metadata will be carried over to the stub ini. - You can also specify `GenerateOnlyNewValuesInTranslationStub=true` in the same place to only output missing values instead of everything in the translation stub, which may be more convenient depending on your workflow. - Non-text values (for instance, size and position) are not written to the stub INI, but you can still write them manually if needed. 1. Create a folder in `Resources/Translations` that uses the desired language code as name (see above) and place `Translation.ini` from `Client` folder there, and start translating the strings and uncommenting the translated ones. - Hardcoded strings are shared between same client binaries and are independent of mods, so you could reuse all the strings with `Client` prefix that you or someone else made for the language you're translating the client to. Or use `[INISystem]->BasedOn= ; INI name` in the main `Translation.ini` to include a separate file (for instance, `ClientTranslation.ini`) with all the `Client`-prefixed strings placed in the same section. - **Caveat:** hardcoded control size/position values are not read from the translation file at all; as a workaround ask the mod author to specify the size/position values that you will adjust using INI definition for that control, so that it can be adjusted using translation system - To speed up the workflow it's advised to use an editor with multi-selection, like [Visual Studio Code](https://code.visualstudio.com), so that you can select values in batches. Select the `=` on the first untranslated line, press `Ctrl+D` as many times as needed to select the remaining `=` on untranslated lines, press `→`, then `Shift+End`. That will select all untranslated values for the lines you marked, so copy them and go to [DeepL](https://www.deepl.com) (recommended) or any other translator, paste the text, correct the translation, copy it back and paste in the same position. VSCode automatically splits the lines back so you don't need to input them one by one. - DeepL also adds it's "translated with" line too, so you might need to paste the text in some intermediate file/window/tab, remove that line, and copy it again. 2. For every translated asset, including theme-specific ones, you must replicate the exact path relative to the `Resources` folder for the original asset in your translation folder. The assets should also be named the same as the original ones. They will automatically override the non-translated ones. 3. In case you need theme-specific translated values - create `Translation.ini` in the theme subfolder of your translation folder and put the needed key-value overrides in `[Values]` section (metadata won't be read from this file; also it won't be read at all if the main `Translation.ini` doesn't exist). 4. (optional) Look up the game/mod-specific ingame translation files that are specified in `ClientDefinitions.ini`->`[Translations]`->`GameFileX` and/or consult the game/mod author(s) for a list of files for ingame translation. Make and arrange your ingame translation into the files with specified names (first part of the value) and place them in your translation folder. - If the game/mod has integrity-checked translation files - contact the game/mod author to include your translation with the game/mod package so the ingame translation won't make your or your users' installations trigger a modified files warning online. Happy translating! ## Miscellanous - Discord presence, game broadcasting, stats etc. use untranslated names so that other players can see the more universal English names, and to not be locked onto a translation in case it changes. - When translated, original map names still display in a tooltip and can be copied via context menu. - Where applicable, both translated and untranslated names are used to search (map and lobby searches). ================================================ FILE: Docs/Updater.md ================================================ # Instructions on how to use the updater functionality of the XNA CnCNet client Updater-Related Files ------------------- ### Developer Files **These files are needed only by the mod developer and aren't meant to be redistributed to others!** - **Version File Writer**: Software that writes a version file for updater. Executable and example config file are [included in client repository](../AdditionalFiles/VersionFileWriter). Source code of the program is available [here](https://github.com/Starkku/VersionWriter). - **Update Server Scripts** (`preupdateexec` and `updateexec`, example files [included in client repository](../AdditionalFiles/UpdateServerScripts)): Script files that can be used to rename, move or delete files & directories. They are downloaded and executed by updater before and after the update, respectively. They can be put on the server in the same folder specified in download mirrors. Note that changes made by `preupdateexec` **will not** be reverted even if the update process itself fails afterwards. Additionally both of the scripts are executed regardless of current local or server version state & info. ### Distributable Files - **Updater Configuration File** (`Resources/UpdaterConfig.ini`, included with [default resources](../DXMainClient/Resources/DTA) in the client repository): Client [updater configuration](#updater-configuration) file which sets the download mirrors for the updater and available custom component info. If no such file is found, client falls back to using legacy `updateconfig.ini` which uses a different syntax and does not allow setting custom component info. - **Second-Stage Updater** (`Resources/Binaries/Updater/SecondStageUpdater.exe`, now belongs to a part of the client binaries: A second-stage updater executable that copies the files to their correct places after they've all been downloaded and then launches the client again after it is done. Client launcher executable is read from `LauncherExe` key in `Resources/ClientDefinitions.ini`, if it is not present or cannot be read for any other reason the client will not automatically restart after the second-stage updater has finished. Basic Usage ----------- ## Quick Guide 1. Have a web server set up and create a publicly accessible directory from which to download your updates from. 2. On your client configuration, add URL of the aforementioned directory to list of available download mirrors in `Resources/UpdaterConfig.ini`. 3. Make changes to files and `VersionConfig.ini`. 4. Run `VersionWriter.exe`. 5. Upload the contents of the `VersionWriter_CopiedFiles` and update server scripts to the aforementioned directory on the web server. ## Detailed Instructions To have automatic updates via XNA CnCNet client an update web server needs to be set up which would then allow the update files to be downloaded by the client during the update process. The URL path to the file (sans update location part) has to replicate the local path to the file relative to mod folder in order to be succesfully downloaded (for example, with update location `https://your.test/location/of/updates/` the file `Resources/Binaries/Windows/clientdx.dll` would need to be accessible at `https://your.test/location/of/updates/Resources/Binaries/Windows/clientdx.dll` URL). Besides the update server scripts, the updater does not explicitly require any other files or specific software to exist or run on the update web server. To set up an update information needed to produce the files to upload on a server edit `VersionConfig.ini` file to include all of the redistributed files (or updated files only if you're saving on bandwidth and don't want to allow full downloads). Each time you need to push an update to your players (also if you change something in `VersionConfig.ini`) you have to change the version key under `[Version]` section in aforementioned configuration file so the CnCNet client prompts for an update. In case you need to force users to download an update manually you can change a key under `[UpdaterVersion]` section. After that run `VersionWriter.exe` and upload the contents of the `VersionWriter_CopiedFiles` to your update server along with updater scripts. Refer below for a more comprehensive explanation of both version writer's and updater's features & configuration files. Features ------- ### Version File Writer Version file writer is a program that writes the `version` file used by the client and its updater. It reads a file called `VersionConfig.ini` from its working directory for settings and list of files to include. The example `VersionConfig.ini` included with the version file writer in client repository contains comments explaining most of the functionality and features. `VersionWriter.exe` accepts command-line arguments that start with `/` or `-` as switches. Following switches are accepted: - `-LOG`: Generates log file in the program directory. - `-QUIET`: Does not generate console output. - `-SUPRESSINPUTS`: Does not ask for user input to confirm actions. Additionally a single non-switch argument can be provided that can be used to set the program's working directory - this allows running VersionWriter from outside the mod directory itself. #### Options These are set under `[Options]` in `VersionConfig.ini`. - `EnableExtendedUpdaterFeatures`: If set, enables additional updater features such as compressed archives, updater version and manual download URL. - `RecursiveDirectorySearch`: If set, will go through every subdirectory recursively for directories given in `[Include]`. - `IncludeOnlyChangedFiles`: If set, version file writer will always create two version files - one with everything included (`version_base`) and the proper, actual version file with only changed files (`version`). Note that `version_base` should be kept around as it is used to compare which files have been changed next time version file writer is ran. - `CopyArchivedOriginalFiles`: If set, original versions of archived files will also be copied to copied files directory. - `ExcludeHiddenAndSystemFiles`: 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`. - `ApplyTimestampOnVersion`: If set, the mod version string is treated as [.NET timestamp/datetime format string](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings) with current local time applied on it. - `NoCopyMode`: 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. #### Updater Version & Manual Download URL Setting `[UpdaterVersion]` in `VersionConfig.ini` writes this information to the `version` file and allows developers to control which versions are allowed to download files from the version info through the client. Mismatching updater versions between local and server version files will suggest users to download update manually through updater status message. Absent or malformed updater version (both local & server) is equivalent to `N/A` and updater will bypass the mismatch check entirely if server updater version is set to this or absent. Additionally setting `[ManualDownloadURL]` will, in addition to displaying the updater status message, also bring up a notification dialog with the provided URL as a download link in case a updater version mismatch occurs. #### Compressed Archives The updater supports downloading and uncompressing LZMA-compressed data archives. Files that are to be compressed should be included under `[ArchiveFiles]` in `VersionConfig.ini`. Note that they still need to be included through `[Include]` in the first place. As a result there would be information in the `version` file which allows the client, to figure out it is supposed to download the archive instead, and instead of the original files the compressed files with `.lzma` extension are placed to the `VersionWriter_CopiedFiles` folder. #### Custom Components Custom components are available even with the original XNA CnCnet Client, but since the IDs and filenames are hardcoded in the updater, their usage is limited. Custom component info for the updater can be set in `Resources/UpdaterConfig.ini`, see below for more info. For version file writer, any custom components should be included under `[AddOns]`, using syntax `ID=filename` as shown in the example `VersionConfig.ini`. Custom component filenames **should not** be listed under `[Include]`. The filenames can be listed under `[ArchiveFiles]` to enable use of compressed archives. - Custom component download file path (in `Resources/UpdaterConfig.ini`) accepts absolute URLs and uses them properly, so it's possible to define custom components which have to be downloaded from elsewhere. ### Updater Configuration The example `Resources/UpdaterConfig.ini` included with client files contains comments explaining most of the functionality and features. The only currently supported global updater setting under `[Settings]` is `IgnoreMasks` that allows customizing the list filenames that are exempted from file integrity checks even if they are included in `version` file. #### Download Mirrors List of available download mirrors from which to download version info and files from. Listed as comma-separated values under `[DownloadMirrors]`, containing URL, UI display name and location. Location is optional and can be omitted. Updater and Updater & Component options in client options will be unavailable if no download mirrors are found. #### Custom Components List of custom components available for the updater. Listed as comma-separated values under `[CustomComponents]`, containing custom component ID used in the `version` file, download path / URL, local filename, flag that disables archive file extensions for download path / URL. Download path / URL supports absolute URLs, allowing custom components to be downloaded from location outside the current update server but also restricts it to one download location instead of one per each download mirror. Download path archive file extension disable flag is a boolean value (yes/no, true/false), is optional and defaults to false. Custom components and the Components tab in client options will be unavailable if no custom component info is found. ================================================ FILE: GitVersion.yml ================================================ # Configuration docs: https://gitversion.net/docs/reference/configuration # # This configuration will allow the gitversion msbuild task to auto version our executable. It uses existing tags # to determine the next version by auto incrementing the Minor, Patch, or Tag version. The Tag version is calculated # by counting the number of commits since the last tag. # Patch version is incremented by 1 when a build is created from the develop or master branch. # # Examples: # # Latest tag is "2.8.0" - # The next commit on develop branch will start version 2.8.1-beta.1. # That increments the Patch by 1. That also increments the beta by 1 for the 1 additional commit. # Another commit creates 2.8.1-beta.2 and so on. # # Latest tag is "2.8.0-beta.1" - # The next commit on develop branch is "2.8.0-beta.2". Patch version is NOT incremented, because the # base tag of "2.8.0-beta.1" is also a beta. # # Latest tag is "2.8.0" - # A new commit is added directly to master. The new version is "2.8.1-rc.1". # That increments the Patch, because it is treated as a "hotfix" directly on master. # # Versioning Modes Quick View: # # Continuous Delivery: The default versioning mode. In this mode, GitVersion calculates the next version and will use that until that is released. # Continuous Deployment: Sometimes you just want the version to keep changing and deploy continuously. # In this case, Continuous Deployment is a good mode to operate GitVersion by. tag-prefix: 'v' branches: master: regex: ^master$ mode: ContinuousDelivery increment: Patch is-mainline: true source-branches: [ 'develop' ] tag: rc develop: regex: ^develop$ increment: Patch is-mainline: false mode: ContinuousDeployment tag: beta ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: NuGet.config ================================================ ================================================ FILE: README.md ================================================ # CnCNet Client The MonoGame / XNA CnCNet client, a platform for playing classic Command & Conquer games and their mods both online and offline. Supports setting up and launching both singleplayer and multiplayer games with [a CnCNet game spawner](https://github.com/CnCNet/ts-patches). Includes an IRC-based chat client with advanced features like private messaging, a friend list, a configurable game lobby, flexible and moddable UI graphics, and extras like game setting configuration and keeping track of match statistics. And much more! You can find the [dedicated project development chat](https://discord.gg/M5gGdBYG5m) at C&C Mod Haven Discord server. ## Targets The primary targets of the client project are * [Dawn of the Tiberium Age](https://www.moddb.com/mods/the-dawn-of-the-tiberium-age) * [Twisted Insurrection](https://www.moddb.com/mods/twisted-insurrection) * [Mental Omega](https://www.moddb.com/mods/mental-omega) * [CnCNet Yuri's Revenge](https://cncnet.org/yuris-revenge) However, there is no limitation in the client that would prevent incorporating it into other projects. Any game or mod project that utilizes the CnCNet spawner for Tiberian Sun and Red Alert 2 can be supported. Several other projects also use the client or an unofficial fork of it, including [Tiberian Sun Client](https://www.moddb.com/mods/tiberian-sun-client), [Project Phantom](https://www.moddb.com/mods/project-phantom), [YR Red-Resurrection](https://www.moddb.com/mods/yr-red-resurrection), [The Second Tiberium War](https://www.moddb.com/mods/the-second-tiberium-war) and [CnC: Final War](https://www.moddb.com/mods/cncfinalwar). ## Development requirements The client supports 2 runtimes: .NET 4.8 and .NET 8.0. * Both runtimes have 3 rendering engines: Windows DirectX11, Windows OpenGL and Windows XNA. * .NET 8.0 in addition has a cross-platform Universal OpenGL engine. * The DirectX11 and OpenGL engines rely on MonoGame. * The XNA engine relies on Microsoft's XNA Framework 4.0 Refresh. To build the client, **you must use Git to clone the repository**, instead of downloading a ZIP archive. After cloning, make sure to **initialize and update the submodules** using the following command: ```shell git submodule update --init --recursive ``` Building for **any** platform requires the .NET SDK 10.0. Editing the source code requires Visual Studio 2026 or newer, or Rider 2025.3 or newer. A modern version of Visual Studio Code also works, but is not officially supported. To debug WindowsXNA builds the .NET SDK 10.0 x86 is additionally required. When using the included build scripts, [PowerShell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows) is required. ## Building and debugging * It is simple to build the client. Assuming you have the .NET SDK 10.0 and PowerShell 7 installed, you can just double-click `Scripts/build.bat` to compile it right away. You can then copy the contents of this `Compiled` directory into the `Resources` sub-directory of any target project. Please turn off Visual Studio while executing scripts. * If you want to run the client in debug mode, open the solution file `DXClient.slnx` using Visual Studio, and select Debug -> Start Debugging (F5). > [!IMPORTANT] > If you switch among different solution configurations in Visual Studio (e.g. switch to `UniversalGLRelease` from `WindowsDXDebug`), especially switching between .NET 4.8 and .NET 8.0 runtimes, **it is highly recommended to restart Visual Studio after switching configurations to prevent unexpected error messages**. If restarting Visual Studio do not work as intended, try deleting all `obj` folders in each project. Due to the same reason, **it is also highly recommended to close Visual Studio when using the scripts in `Scripts` folder**. ### Advanced notes on building and debugging * When built as a debug build, the client executable expects to reside in the same directory with the target project's main game executable. Resources should exist in a "Resources" sub-directory in the same directory. The repository contains sample resources and post-build commands for copying them so that you can immediately run the client in debug mode by just hitting the Debug button in Visual Studio. * When built in release mode, the client executables expect to reside in the `Resources` sub-directory itself for .NET 4.8, named `clientdx.exe`, `clientogl.exe` and `clientxna.exe`. Each `.exe` file or `.dll` file expects a `.pdb` file for diagnostics purpose. It's advised not to delete these `.pdb` files. Keep all `.pdb` files even for end users. * For .NET 8, When built in release mode, the client executables expect to reside in `Resources/BinariesNET8/{Windows, OpenGL, UniversalGL, XNA}` folders, named `client{dx, ogl, ogl, xna}.dll`, respectively. Note that `client{dx, ogl, ogl, xna}.runtimeconfig.json` files are required for the corresponding .NET 8 DLLs. When built on an OS other than Windows, only the Universal OpenGL engine is available. * Some dependencies are stored in `References` folder instead of the official NuGet source. This folder is also useful if you are working on modifying a dependency and debugging in your local machine without publishing the modification to NuGet. However, if you have replaced the `.(s)nupkg` files of a package, without altering the package version, be sure to remove the corresponding package from `%USERPROFILE%\.nuget\packages` folder (Windows) to purge the old version. Refer to [Docs/Build.md](/Docs/Build.md) for more information about building the client. ## End-user requirements * Windows: Windows 7 SP1 or higher is required. The preferred rendering engine is DirectX11 (.NET 4.8), i.e., `clientdx.exe`. If your GPU does not support DX11, consider using the OpenGL or XNA engine instead. Advanced users may experiment with .NET 8 runtime at their discretion. * Other OS: Use the Universal OpenGL engine. ### Windows .NET 4.8 requirements: * The [.NET Framework 4.8 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet-framework/thank-you/net48-web-installer) (Optional) The XNA engine requires: * [Microsoft XNA Framework Redistributable 4.0 Refresh](https://www.microsoft.com/en-us/download/details.aspx?id=27598). ### Linux requirements: * The [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime?initial-os=linux) for your specific platform. ### macOS requirements: * The [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime?initial-os=macos) for your specific platform. ### Windows .NET 8.0 requirements:
Windows .NET 8.0 requirements * The [.NET 8.0 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime?initial-os=windows) for your specific platform. (Optional) The XNA engine requires: * [Microsoft XNA Framework Redistributable 4.0 Refresh](https://www.microsoft.com/en-us/download/details.aspx?id=27598). * [.NET 8.0 Desktop Runtime x86](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.0-windows-x86-installer). Windows 7 SP1 and Windows 8.x additionally require: * Microsoft Visual C++ 2015-2019 Redistributable [64-bit](https://aka.ms/vs/16/release/vc_redist.x64.exe) / [32-bit](https://aka.ms/vs/16/release/vc_redist.x86.exe). Note: the latest version of this redistributable is named "Microsoft Visual C++ 2015-2026 Redistributable", available [here](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist). We recommend using the latest version instead of the 2015-2019 version. Windows 7 SP1 additionally requires: * KB3063858 [64-bit](https://www.microsoft.com/download/details.aspx?id=47442) / [32-bit](https://www.microsoft.com/download/details.aspx?id=47409).
## Client launcher This repository does not contain the client launcher (for example, `DTA.exe` in Dawn of the Tiberium Age) that selects which platform's client executable is most suitable for each user's system. See [xna-cncnet-client-launcher](https://github.com/CnCNet/xna-cncnet-client-launcher). ## Branches Currently there are only two major active branches. `develop` is where development happens, and while things should be fairly stable, occasionally there can also be bugs. If you want stability and reliability, the `master` branch is recommended. ## Screenshots ![Screenshot](cncnetchatlobby.png?raw=true "CnCNet IRC Chat Lobby") ![Screenshot](cncnetgamelobby.png?raw=true "CnCNet Game Lobby") ## License CnCNet Client Copyright (C) 2013-2026 CnCNet, Rampastring This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ### Additional permission under GNU GPL version 3 section 7 If you modify this program, or any covered work, by linking or combining it with the Steamworks SDK (or a modified version of that library), containing parts covered by the terms of the Steamworks SDK's license, the licensors of this program grant you additional permission to convey the resulting work. ## Sponsored by Powered By Digital Ocean ================================================ FILE: References/.gitkeep ================================================ ================================================ FILE: Scripts/Build.bat ================================================ @echo off where pwsh > nul 2> nul if %errorlevel% equ 0 ( pwsh -ExecutionPolicy Bypass -File build.ps1 ) else ( echo "Please Install PowerShell." echo "https://aka.ms/pscore6" ) pause ================================================ FILE: Scripts/ClearBinAndObjDirs.bat ================================================ @echo off cd /d %~dp0 cd .. for /f "tokens=*" %%f in ('dir ".\" /a:d /b') do ( rmdir /q /s "%%f\bin" > nul 2> nul rmdir /q /s "%%f\obj" > nul 2> nul ) ================================================ FILE: Scripts/Get-CommonAssemblyList.ps1 ================================================ #!/usr/bin/env pwsh #Requires -Version 7.2 # /// WARNING /// WARNING /// WARNING /// # # DO NOT CHANGE OUTPUT FOR THIS SCRIPT! # # /// WARNING /// WARNING /// WARNING /// # This script generates a list of generic assemblies by calculating the contents generated by the `. \build.ps1 -NoMove` command. [CmdletBinding()] param ( [Parameter()] [switch] $Net8 ) [string]$Script:RepoRoot = Split-Path $PSScriptRoot [string]$Script:CompiledRoot = Join-Path $RepoRoot 'Compiled' [string]$Script:GamePath = $CompiledRoot [string]$Script:Resources = Join-Path $GamePath 'Resources' [string]$Script:Binaries = Join-Path $Resources 'Binaries' if ($Net8) { $Script:Binaries = Join-Path $Resources 'BinariesNET8' } [System.Collections.Generic.List[string]]$Script:Engines = @('OpenGL', 'Windows', 'XNA') if ($Net8) { $Script:Engines.Add('UniversalGL') } [hashtable]$Script:FileHashTable = @{} $Script:Engines | ForEach-Object { [string]$Private:Engine = $PSItem [string]$Private:PlatformFolder = Join-Path $Binaries $Private:Engine Get-ChildItem $Private:PlatformFolder | Where-Object { $PSItem -is [System.IO.FileInfo] } | ForEach-Object { if (!$Script:FileHashTable.ContainsKey($PSItem.Name)) { $Script:FileHashTable[$PSItem.Name] = [hashtable]@{} } $Script:FileHashTable[$PSItem.Name][$Engine] = Get-FileHash $PSItem } } $Script:FileList = $Script:FileHashTable.Keys | Where-Object { $Private:Key = $PSItem if ($Script:FileHashTable[$Private:Key].Count -ne $Script:Engines.Count) { return $false } [string]$hash = $null foreach ($item in $Script:FileHashTable[$Private:Key].Values) { if ([string]::IsNullOrEmpty($hash)) { $hash = $item.Hash } elseif ($hash -ne $item.Hash) { return $false } } return $true } $Script:FileList | Sort-Object -Unique ================================================ FILE: Scripts/README.md ================================================ # README for Build Scripts > [!NOTE] > Before running any scripts in this folder, please close Visual Studio. ## Build the client Double-click the following script file: `Build.bat`. ## Update the common assembly list You should do this if you have introduced any new NuGet dependencies. 1. Launch Powershell (`pwsh`, not `PowerShell`) and switch to this folder. 2. `.\build.ps1 -NoMove` 3. `.\Get-CommonAssemblyList.ps1 -Net8 > ..\CommonAssemblies.txt` 4. `.\Get-CommonAssemblyList.ps1 > ..\CommonAssembliesNetFx.txt` 5. Carefully check the changes with Git diff: - If you have introduce new NuGet dependencies, check if they have appeared in the list. - If they do show in the list, it's expected. - If they do not show there, **do not** manually add them to the list. Think carefully about whether these libraries should differ among DX/GL/XNA builds. - If there are other libraries get **removed** from this list, don't just commit the changes. Does this library exist in the `Compiled` folder? - If so, we can **resume** this line instead of removing it. - If not, think carefully if we should keep this item, depending on whether these libraries should differ among DX/GL/XNA builds. - Specifially, we intend to leave `ClientUpdater.dll` and `ClientUpdater.pdb` files in that list since we *know* this library does not differ among DX/GL/XNA builds, regardless the fact that these two files are different among DX/GL/XNA builds. - If there are other libraries just get **added** in this list, check if such a library has already been shown up in **previous** releases of the client. - If so, we should **delete** such a line, because a library showing in this list has a lower priority than the library that is not included in this list. - If not, we can keep the changes. This means a commit after the latest release brought another dependency and **forgot** to update the common assembly list. It's lucky we catch it up before making a new release. Note: if this dependency change is unrelated with your current PR, don't mix it up in the current PR, but rather, use a separate PR to update the forgotten dependency in the common assembly list. 6. Delete the `Compiled` folder since it is produced with `-NoMove` parameter. We should absolutely **not** distribute these files. ================================================ FILE: Scripts/build.ps1 ================================================ #!/usr/bin/env pwsh #Requires -Version 7.2 ##################################################################### # # Note: # Be careful to synchronize changes to `Directory.Build.targets` # when making changes to paths. # ##################################################################### <# .SYNOPSIS Builds XNA CnCNet Client using specified parameters. .DESCRIPTION You can use this script to make publish packages for your game. .PARAMETER IsDebug Build projects in debug mode. .PARAMETER Log Detail log. .PARAMETER NoClean Do not clean Compiled folder. .PARAMETER NoMove Do not make folder structure. .EXAMPLE build.ps1 Build. .EXAMPLE build.ps1 -IsDebug Build on debug mode. #> param( [Parameter()] [switch] $IsDebug, [Parameter()] [switch] $Log, [Parameter()] [switch] $NoClean, [Parameter()] [switch] $NoMove ) $Script:ConfigurationSuffix = 'Release' if ($IsDebug) { $Script:ConfigurationSuffix = 'Debug' } $Script:RepoRoot = Split-Path $PSScriptRoot $Script:ProjectPath = Join-Path $RepoRoot 'DXMainClient' 'DXMainClient.csproj' $Script:CompiledRoot = Join-Path $RepoRoot 'Compiled' $Script:EngineSubFolderMap = @{ 'UniversalGL' = 'UniversalGL' 'WindowsDX' = 'Windows' 'WindowsGL' = 'OpenGL' 'WindowsXNA' = 'XNA' } $Script:FrameworkBinariesFolderMap = @{ 'net48' = 'Binaries' 'net8.0' = 'BinariesNET8' 'net8.0-windows' = 'BinariesNET8' } if (!$NoClean -AND (Test-Path $Script:CompiledRoot)) { Remove-Item -Recurse -Force -LiteralPath $Script:CompiledRoot } if ($null -EQ $IsWindows -AND 'Desktop' -EQ $PSEdition) { $Script:IsWindows = $true } function Script:Invoke-BuildProject { [CmdletBinding(DefaultParameterSetName = 'ByGame')] param ( [Parameter(Mandatory, ParameterSetName = 'Detail', Position = 0)] [string] $Engine, [Parameter(Mandatory, ParameterSetName = 'Detail')] [string] $Framework ) process { if ($Engine) { $Output = Join-Path $CompiledRoot 'Resources' ($FrameworkBinariesFolderMap[$Framework]) ($EngineSubFolderMap[$Engine]) $Private:ArgumentList = [System.Collections.Generic.List[string]]::new(11) $Private:ArgumentList.Add('publish') $Private:ArgumentList.Add("$ProjectPath") $Private:ArgumentList.Add('--graph') $Private:ArgumentList.Add("--configuration:${Engine}$Script:ConfigurationSuffix") $Private:ArgumentList.Add("--framework:$Framework") $Private:ArgumentList.Add("--output:$Output") $Private:ArgumentList.Add('-property:SatelliteResourceLanguages=en') if ($Log) { $Private:ArgumentList.Add('-verbosity:diagnostic') } if ($NoMove) { $Private:ArgumentList.Add('-property:NoMove=true') } # $Private:ArgumentList.Add("-property:AssemblyVersion=$AssemblySemVer") # $Private:ArgumentList.Add("-property:FileVersion=$AssemblySemFileVer") # $Private:ArgumentList.Add("-property:InformationalVersion=$InformationalVersion") # if ($Engine -eq 'WindowsXNA') { # $Private:ArgumentList.Add('--arch=x86') # } & 'dotnet' $Private:ArgumentList if ($LASTEXITCODE) { throw "Build failed for ${Engine}$Script:ConfigurationSuffix $Framework (exit code $LASTEXITCODE)" } } else { Invoke-BuildProject -Engine 'UniversalGL' -Framework 'net8.0' if ($IsWindows) { @('WindowsDX', 'WindowsGL', 'WindowsXNA') | ForEach-Object { $Private:Engine = $PSItem @('net48', 'net8.0-windows') | ForEach-Object { $Private:Framework = $PSItem Invoke-BuildProject -Engine $Private:Engine -Framework $Private:Framework } } } } } } Script:Invoke-BuildProject ================================================ FILE: SecondStageUpdater/Program.cs ================================================ // Copyright 2022-2025 CnCNet // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY, without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #pragma warning disable IDE0057 // Use range operator namespace SecondStageUpdater; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using Rampastring.Tools; internal sealed class Program { private const int MutexTimeoutInSeconds = 30; private static ConsoleColor defaultColor; private static bool hasHandle; private static Mutex clientMutex; // e.g. args = ["clientogl.dll", "\"C:\\Game\\\""]; private static void Main(string[] args) { defaultColor = Console.ForegroundColor; try { Write("CnCNet Client Second-Stage Updater", true, ConsoleColor.Green); Write(string.Empty); if (args.Length < 2 || string.IsNullOrEmpty(args[0]) || string.IsNullOrEmpty(args[1])) { Write("Invalid arguments given!", true, ConsoleColor.Red); Write("Usage: "); Write(string.Empty); Exit(false); } DirectoryInfo baseDirectory = SafePath.GetDirectory(args[1].Replace("\"", null)); if (!baseDirectory.Exists) { Write("Base directory does not exist!", true, ConsoleColor.Red); Write(baseDirectory.FullName); Write(string.Empty); Exit(false); } else { string clientExecutable = args[0]; DirectoryInfo resourceDirectory = SafePath.GetDirectory(baseDirectory.FullName, "Resources"); FileInfo logFile = SafePath.GetFile(SafePath.CombineFilePath(baseDirectory.FullName, "Client", "SecondStageUpdater.log")); if (logFile.Exists) logFile.Delete(); Logger.Initialize(logFile.DirectoryName, logFile.Name); Logger.WriteLogFile = true; Logger.WriteToConsole = false; Logger.Log("CnCNet Client Second-Stage Updater"); Logger.Log("Version: " + GitVersionInformation.AssemblySemVer); Write("Base directory: " + baseDirectory.FullName); Write($"Waiting for the client ({clientExecutable}) to exit.."); // note: the GUID should be consistent with the one in xna-cncnet-client/DXMainClient/Program.cs string clientMutexId = FormattableString.Invariant($"Global{Guid.Parse("1CC9F8E7-9F69-4BBC-B045-E734204027A9")}"); clientMutex = new(false, clientMutexId, out _); try { hasHandle = clientMutex.WaitOne(TimeSpan.FromSeconds(MutexTimeoutInSeconds), false); } catch (AbandonedMutexException) { hasHandle = true; } if (!hasHandle) { Write($"Timeout while waiting for the client ({clientExecutable}) to exit!", true, ConsoleColor.Red); Exit(false); } // This is occasionally necessary to prevent DLLs from being locked at the time that this update is attempting to overwrite them Thread.Sleep(1000); DirectoryInfo updaterDirectory = SafePath.GetDirectory(baseDirectory.FullName, "Updater"); if (!updaterDirectory.Exists) { Write($"{updaterDirectory.Name} directory does not exist!", true, ConsoleColor.Red); Exit(false); } Write("Updating files.", true, ConsoleColor.Green); IEnumerable files = updaterDirectory.EnumerateFiles("*", SearchOption.AllDirectories); FileInfo executableFile = SafePath.GetFile(Assembly.GetExecutingAssembly().Location); FileInfo relativeExecutableFile = SafePath.GetFile(executableFile.FullName.Substring(baseDirectory.FullName.Length)); const string versionFileName = "version"; Write($"{nameof(SecondStageUpdater)}: {relativeExecutableFile}"); AssemblyName[] assemblies = Assembly.LoadFrom(executableFile.FullName).GetReferencedAssemblies(); foreach (FileInfo fileInfo in files) { string relativeFileName = fileInfo.FullName.Substring(updaterDirectory.FullName.Length); string fileExtension = fileInfo.Extension; string relativeFileNameWithoutExtension = relativeFileName.Substring(0, relativeFileName.Length - fileExtension.Length); string relativeExecutableFileFullName = relativeExecutableFile.FullName; string relativeExecutableFileExtension = relativeExecutableFile.Extension; string relativeExecutableFileFullNameWithoutExtension = relativeExecutableFileFullName.Substring(0, relativeExecutableFileFullName.Length - relativeExecutableFileExtension.Length); if (relativeFileNameWithoutExtension.Equals(relativeExecutableFileFullNameWithoutExtension, StringComparison.OrdinalIgnoreCase) || relativeFileNameWithoutExtension.Equals(SafePath.CombineFilePath("Resources", Path.GetFileNameWithoutExtension(relativeExecutableFile.Name)), StringComparison.OrdinalIgnoreCase)) { Write($"Skipping {nameof(SecondStageUpdater)} file {relativeFileName}"); } else if (assemblies.Any(q => relativeFileNameWithoutExtension.Equals(q.Name, StringComparison.OrdinalIgnoreCase)) || assemblies.Any(q => relativeFileNameWithoutExtension.Equals(SafePath.CombineFilePath("Resources", q.Name), StringComparison.OrdinalIgnoreCase))) { Write($"Skipping {nameof(SecondStageUpdater)} dependency {relativeFileName}"); } else if (relativeFileName.Equals(versionFileName, StringComparison.OrdinalIgnoreCase)) { Write($"Skipping {relativeFileName}"); } else { try { FileInfo copiedFile = SafePath.GetFile(baseDirectory.FullName, relativeFileName); Write($"Updating {relativeFileName}"); // If the file is read-only, we need to remove the read-only attribute before copying it if (copiedFile.Exists && copiedFile.IsReadOnly) { copiedFile.IsReadOnly = false; fileInfo.CopyTo(copiedFile.FullName, true); copiedFile.IsReadOnly = true; } else { fileInfo.CopyTo(copiedFile.FullName, true); } } catch (Exception ex) { Write($"Updating file failed! Returned error message: {ex}", true, ConsoleColor.Yellow); Write("If the problem persists, try to move the content of the \"Updater\" directory to the main directory manually or contact the staff for support."); Exit(false); } } } FileInfo versionFile = SafePath.GetFile(updaterDirectory.FullName, versionFileName); if (versionFile.Exists) { FileInfo destinationFile = SafePath.GetFile(baseDirectory.FullName, versionFile.Name); FileInfo relativeFileInfo = SafePath.GetFile(destinationFile.FullName.Substring(baseDirectory.FullName.Length)); Write($"Updating {relativeFileInfo}"); versionFile.CopyTo(destinationFile.FullName, true); } Write("Files successfully updated. Starting launcher..", true, ConsoleColor.Green); string launcherExe = string.Empty; try { Write("Checking ClientDefinitions.ini for launcher executable filename."); string[] lines = File.ReadAllLines(SafePath.CombineFilePath(resourceDirectory.FullName, "ClientDefinitions.ini")); string launcherPropertyName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "LauncherExe" : "UnixLauncherExe"; string line = lines.Single(q => q.Trim().StartsWith(launcherPropertyName, StringComparison.OrdinalIgnoreCase) && q.Contains('=')); int commentStart = line.IndexOf(";", StringComparison.OrdinalIgnoreCase); if (commentStart >= 0) line = line.Substring(0, commentStart); launcherExe = line.Split('=')[1].Trim(); } catch (Exception ex) { Write($"Failed to read ClientDefinitions.ini: {ex}", true, ConsoleColor.Yellow); } FileInfo architectureLauncherExeFile = SafePath.GetFile(resourceDirectory.FullName, "Launcher", FormattableString.Invariant($"{Path.GetFileNameWithoutExtension(launcherExe)}-{RuntimeInformation.OSArchitecture}{Path.GetExtension(launcherExe)}")); FileInfo launcherExeFile = SafePath.GetFile(baseDirectory.FullName, launcherExe); if (architectureLauncherExeFile.Exists) { architectureLauncherExeFile.CopyTo(launcherExeFile.FullName, true); launcherExeFile.Refresh(); } if (launcherExeFile.Exists) { Write("Launcher executable found: " + launcherExe, true, ConsoleColor.Green); using var _ = Process.Start(new ProcessStartInfo { FileName = launcherExeFile.FullName }); } else { Write("No suitable launcher executable found! Client will not automatically start after updater closes.", true, ConsoleColor.Yellow); Exit(false); } } Exit(true); } catch (Exception ex) { Write("An error occurred during the Launcher Updater's operation.", true, ConsoleColor.Red); Write($"Returned error was: {ex}"); Write(string.Empty); Write("If you were updating a game, please try again. If the problem continues, contact the staff for support."); Exit(false); } } private static void Exit(bool success) { if (hasHandle) { clientMutex.ReleaseMutex(); clientMutex.Dispose(); } if (!success) { Write("Press any key to exit."); Console.ReadKey(); Environment.Exit(1); } } private static void Write(string text, bool logToFile = true, ConsoleColor? color = null) { Console.ForegroundColor = color ?? defaultColor; Console.WriteLine(text); if (logToFile) Logger.Log(text); } } ================================================ FILE: SecondStageUpdater/SecondStageUpdater.csproj ================================================ Exe false CnCNet.SecondStageUpdater CnCNet Client Second-Stage Updater CnCNet.SecondStageUpdater ================================================ FILE: TranslationNotifierGenerator/StringExtensions.cs ================================================ using Microsoft.CodeAnalysis.CSharp; namespace TranslationNotifierGenerator { public static class StringExtensions { public static string ToLiteral(this string input) { // https://stackoverflow.com/a/55798623 return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(input)).ToFullString(); } } } ================================================ FILE: TranslationNotifierGenerator/TranslationNotifierGenerator.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace TranslationNotifierGenerator { /// /// Generates a TranslationNotifier class that allows to notify the translation system /// about all hardcoded missing translation strings by calling TranslationNotifier.Register(). /// /// /// It is required to make RootNamespace project property visible to the compiler via CompilerVisibleProperty /// (already handled in Directory.Build.props). This is required to generate the correct namespace for the generated class. /// [Generator] public class TranslationNotifierGenerator : ISourceGenerator { // Change those if you change the method names public const string LocalizeMethodContainingNamespace = "ClientCore.Extensions"; public const string LocalizeMethodName = "L10N"; public void Execute(GeneratorExecutionContext context) { // uncomment to debug the generator //Debug.WriteLine($"Executing {nameof(TranslationNotifierGenerator)}..."); var compilation = context.Compilation; _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.RootNamespace", out string namespaceName); if (!namespaceName.Split(new char[] { '.' }).All(name => SyntaxFacts.IsValidIdentifier(name))) throw new Exception("The namespace can not contain invalid characters."); Dictionary translations = new(); foreach (var tree in compilation.SyntaxTrees) { context.CancellationToken.ThrowIfCancellationRequested(); // https://stackoverflow.com/questions/43679690/with-roslyn-find-calling-method-from-string-literal-parameter var memberAccessSyntaxes = tree.GetRoot().DescendantNodes().OfType(); foreach (var memberAccessSyntax in memberAccessSyntaxes) { context.CancellationToken.ThrowIfCancellationRequested(); if (memberAccessSyntax == null || !memberAccessSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression) || memberAccessSyntax.Name.ToString() != LocalizeMethodName) { continue; } if (memberAccessSyntax.Parent is not InvocationExpressionSyntax l10nSyntax || l10nSyntax.ArgumentList.Arguments.Count == 0 || !l10nSyntax.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression)) { continue; } // https://stackoverflow.com/questions/35670115/how-to-use-roslyn-to-get-compile-time-constant-value var semanticModel = compilation.GetSemanticModel(l10nSyntax.SyntaxTree); // Get the key and the value. var keyNameSyntax = l10nSyntax.ArgumentList.Arguments[0]; string keyName = semanticModel.GetConstantValue(keyNameSyntax.Expression).Value?.ToString(); Debug.Assert(keyName is null == !keyNameSyntax.Expression.IsKind(SyntaxKind.StringLiteralExpression)); bool keyNameIsPotentiallyForIni = keyName is null || keyName.StartsWith("INI:"); var valueTextSyntax = l10nSyntax.Expression as MemberAccessExpressionSyntax; string valueText = semanticModel.GetConstantValue(valueTextSyntax.Expression).Value?.ToString(); if (keyNameIsPotentiallyForIni) { if (valueText is not null) { context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor( "CNCNET0001", "Literal INI translation value", "The value of an INI translation should not be a compile-time string.", "CNCNET", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation())); } continue; } if (keyName is not null && valueText is null) { context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor( "CNCNET0002", "Non-literal translation value", $"Failed to get the value of key {keyName} as a compile-time string.", "CNCNET", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation())); continue; } // Check for duplicates. if (translations.ContainsKey(keyName)) { if (valueText != translations[keyName]) { context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor( "CNCNET0003", "Conflict translation items", $"Key {keyName} is defined more than once and the values are not the same.", "CNCNET", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation())); } continue; } // Avoid trimmable strings if (valueText.Trim() != valueText) { context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor( "CNCNET0004", "Trimmable translation value", $"The value of key {keyName} should not have leading or trailing whitespace.", "CNCNET", DiagnosticSeverity.Warning, isEnabledByDefault: true), l10nSyntax.GetLocation())); } translations.Add(keyName, valueText); } } context.CancellationToken.ThrowIfCancellationRequested(); var sb = new StringBuilder(); _ = sb.AppendLine(@" using System.Collections.Generic;"); _ = sb.AppendLine($"using {LocalizeMethodContainingNamespace};"); _ = sb.AppendLine($"namespace {namespaceName}.Generated;"); _ = sb.AppendLine(@" public class TranslationNotifier { public static void Register() {"); foreach (var kv in translations) _ = sb.AppendLine($" {kv.Value.ToLiteral()}.{LocalizeMethodName}({kv.Key.ToLiteral()});"); _ = sb.AppendLine(@" } } "); context.CancellationToken.ThrowIfCancellationRequested(); context.AddSource($"TranslationNotifier.Generated.cs", sb.ToString()); } public void Initialize(GeneratorInitializationContext context) { // uncomment to debug the generator //if (!Debugger.IsAttached) // Debugger.Launch(); //Debug.WriteLine($"Initalized {nameof(TranslationNotifierGenerator)}..."); } } } ================================================ FILE: TranslationNotifierGenerator/TranslationNotifierGenerator.csproj ================================================ netstandard2.0 all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: global.json ================================================ { "sdk": { "rollForward": "latestFeature", "version": "10.0.100" } }